feat(core-testing): RecordingMetrics test double

This commit is contained in:
2026-05-11 11:59:46 +02:00
parent 9835752c21
commit f2627890be
3 changed files with 156 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
export { RecordingTracer, type RecordedSpan } from "./recording-tracer";
export { RecordingLogger, type RecordedCapture } from "./recording-logger";
export { RecordingMetrics, type RecordedMetric } from "./recording-metrics";
export { RecordingJobQueue } from "./recording-job-queue";
export { RecordingEventBus } from "./recording-event-bus";
export { RecordingRealtimeBroadcaster } from "./recording-realtime-broadcaster";

View File

@@ -0,0 +1,89 @@
import { describe, it, expect } from "vitest";
import { RecordingMetrics } from "@/instrumentation/recording-metrics";
describe("RecordingMetrics", () => {
it("records counter calls with kind, name, value, and attributes", () => {
const recording = new RecordingMetrics();
recording.counter("http.requests", 1, { method: "GET", route: "/api/me" });
expect(recording.metrics).toHaveLength(1);
const [m] = recording.metrics;
expect(m!.kind).toBe("counter");
expect(m!.name).toBe("http.requests");
expect(m!.value).toBe(1);
expect(m!.attributes).toEqual({ method: "GET", route: "/api/me" });
});
it("counter() defaults value to 1 when omitted", () => {
const recording = new RecordingMetrics();
recording.counter("events.signups");
expect(recording.metrics[0]!.value).toBe(1);
});
it("records histogram calls", () => {
const recording = new RecordingMetrics();
recording.histogram("http.duration", 123, { route: "/api/list" });
expect(recording.metrics).toHaveLength(1);
const [m] = recording.metrics;
expect(m!.kind).toBe("histogram");
expect(m!.name).toBe("http.duration");
expect(m!.value).toBe(123);
expect(m!.attributes).toEqual({ route: "/api/list" });
});
it("records gauge calls", () => {
const recording = new RecordingMetrics();
recording.gauge("queue.depth", 42, { queue: "emails" });
expect(recording.metrics).toHaveLength(1);
const [m] = recording.metrics;
expect(m!.kind).toBe("gauge");
expect(m!.name).toBe("queue.depth");
expect(m!.value).toBe(42);
expect(m!.attributes).toEqual({ queue: "emails" });
});
it("accumulates multiple calls across all kinds", () => {
const recording = new RecordingMetrics();
recording.counter("a");
recording.histogram("b", 10);
recording.gauge("c", 5);
expect(recording.metrics).toHaveLength(3);
expect(recording.metrics.map((m) => m.kind)).toEqual([
"counter",
"histogram",
"gauge",
]);
});
it("reset() clears all recorded metrics", () => {
const recording = new RecordingMetrics();
recording.counter("x");
recording.histogram("y", 1);
expect(recording.metrics).toHaveLength(2);
recording.reset();
expect(recording.metrics).toHaveLength(0);
});
it("find() returns the first matching metric", () => {
const recording = new RecordingMetrics();
recording.counter("a");
recording.histogram("b", 10);
const found = recording.find((m) => m.kind === "histogram");
expect(found).toBeDefined();
expect(found!.name).toBe("b");
});
it("find() returns undefined when no metric matches", () => {
const recording = new RecordingMetrics();
recording.counter("a");
const found = recording.find((m) => m.kind === "gauge");
expect(found).toBeUndefined();
});
});

View File

@@ -0,0 +1,66 @@
// Local type alias matching the contract in @repo/core-shared/instrumentation.
// Kept inline to avoid a build-graph cycle between core-testing and core-shared.
type MetricAttributeValue = string | number | boolean;
interface IMetrics {
counter(
name: string,
value?: number,
attributes?: Record<string, MetricAttributeValue>,
): void;
histogram(
name: string,
value: number,
attributes?: Record<string, MetricAttributeValue>,
): void;
gauge(
name: string,
value: number,
attributes?: Record<string, MetricAttributeValue>,
): void;
}
export type RecordedMetric = {
kind: "counter" | "histogram" | "gauge";
name: string;
value: number;
attributes: Record<string, MetricAttributeValue>;
};
export class RecordingMetrics implements IMetrics {
metrics: RecordedMetric[] = [];
counter(
name: string,
value = 1,
attributes: Record<string, MetricAttributeValue> = {},
): void {
this.metrics.push({ kind: "counter", name, value, attributes });
}
histogram(
name: string,
value: number,
attributes: Record<string, MetricAttributeValue> = {},
): void {
this.metrics.push({ kind: "histogram", name, value, attributes });
}
gauge(
name: string,
value: number,
attributes: Record<string, MetricAttributeValue> = {},
): void {
this.metrics.push({ kind: "gauge", name, value, attributes });
}
reset(): void {
this.metrics = [];
}
find(
predicate: (m: RecordedMetric) => boolean,
): RecordedMetric | undefined {
return this.metrics.find(predicate);
}
}