import { describe, expect, it } from "vitest"; import type { LogDeps, LogRecord, LogSink } from "../contracts/logging.js"; import { createLogger } from "./logger.js"; function harness() { let idCounter = 0; const deps: LogDeps = { now: () => 1000 + idCounter * 10, newId: () => `span-${++idCounter}`, }; const records: LogRecord[] = []; const sink: LogSink = { emit: (r) => records.push(r) }; return { logger: createLogger({ extensionId: "test" }, sink, deps), records }; } describe("createLogger child-bound attributes", () => { it("merges child-bound attrs into BOTH span-open and span-close records", () => { const { logger, records } = harness(); // Bind `warm: true` via child() — mirrors the cache-warming capture path. const warmLogger = logger.child({ conversationId: "c1", attrs: { warm: true } }); const span = warmLogger.span("provider.request", { model: "x" }); span.end({ attrs: { "usage.cacheReadTokens": 0 } }); const open = records.find((r) => r.kind === "span-open"); const close = records.find((r) => r.kind === "span-close"); // Open carries the bound attr (pre-existing behavior). expect(open?.attributes?.warm).toBe(true); // Close MUST carry it too, so a `warm = true` query finds the closed span // (with its usage/status) — not just the open record. expect(close?.attributes?.warm).toBe(true); // Span-specific attrs from span()/end() are still present on close. expect(close?.attributes?.model).toBe("x"); expect(close?.attributes?.["usage.cacheReadTokens"]).toBe(0); }); it("omits attributes entirely when neither bound nor span attrs exist", () => { const { logger, records } = harness(); const span = logger.span("bare"); span.end(); const close = records.find((r) => r.kind === "span-close"); expect(close?.attributes).toBeUndefined(); }); });