import { beforeEach, describe, expect, it } from "vitest"; import { createBus } from "../bus/bus.js"; import type { AuthContract } from "../contracts/auth.js"; import type { ConfigAccess, EventsEmitter, Extension, HostAPI, Manifest, ManifestContributions, PermissionDecision, PermissionGate, PermissionRequest, ScheduledJob, SecretsAccess, StorageNamespace, } from "../contracts/extension.js"; import { defineEventHook, defineFilter, defineService } from "../contracts/hooks.js"; import type { Attributes, ErrorAttributes, LogDeps, Logger, LogRecord, LogSink, } from "../contracts/logging.js"; import type { ProviderContract } from "../contracts/provider.js"; import type { ToolContract } from "../contracts/tool.js"; import { createHost, type HostDeps, KERNEL_API_VERSION } from "./host.js"; interface FakeLogger extends Logger { readonly logs: Array<{ level: string; message: string; attrs?: Attributes | ErrorAttributes }>; } function createFakeLogger(): FakeLogger { const logs: Array<{ level: string; message: string; attrs?: Attributes | ErrorAttributes }> = []; return { logs, debug: (message: string, attrs?: Attributes) => { if (attrs !== undefined) { logs.push({ level: "debug", message, attrs }); } else { logs.push({ level: "debug", message }); } }, info: (message: string, attrs?: Attributes) => { if (attrs !== undefined) { logs.push({ level: "info", message, attrs }); } else { logs.push({ level: "info", message }); } }, warn: (message: string, attrs?: Attributes) => { if (attrs !== undefined) { logs.push({ level: "warn", message, attrs }); } else { logs.push({ level: "warn", message }); } }, error: (message: string, attrs?: ErrorAttributes) => { if (attrs !== undefined) { logs.push({ level: "error", message, attrs }); } else { logs.push({ level: "error", message }); } }, child( _ctx: Partial & { readonly attrs?: Attributes }, ): Logger { return createFakeLogger(); }, span(_name: string, _attrs?: Attributes): import("../contracts/logging.js").Span { return { id: "fake-span", log: createFakeLogger(), setAttributes() {}, addLink() {}, child() { return this; }, end() {}, }; }, }; } function createFakeLogSink(): LogSink & { readonly records: LogRecord[] } { const records: LogRecord[] = []; return { records, emit: (record: LogRecord) => { records.push(record); }, }; } function createFakeLogDeps(): LogDeps { let idCounter = 0; return { now: () => 1000 + idCounter * 100, newId: () => `span-${++idCounter}`, }; } function createFakeConfig(): ConfigAccess { return { get: () => undefined, getAll: () => ({}), }; } function createFakeStorageFactory(): (ns: string) => StorageNamespace { const stores = new Map>(); return (ns: string) => { let store = stores.get(ns); if (!store) { store = new Map(); stores.set(ns, store); } const s = store; return { get: async (key: string) => s.get(key) ?? null, set: async (key: string, value: string) => { s.set(key, value); }, delete: async (key: string) => { s.delete(key); }, has: async (key: string) => s.has(key), keys: async () => [...s.keys()], }; }; } function createFakeSecrets(): SecretsAccess { const store = new Map(); return { get: async (key: string) => store.get(key) ?? null, set: async (key: string, value: string) => { store.set(key, value); }, delete: async (key: string) => { store.delete(key); }, }; } function createFakePermissions(): PermissionGate { return { check: async (_request: PermissionRequest): Promise => ({ allowed: true, }), }; } function createFakeScheduler(): { readonly register: (job: ScheduledJob) => void; readonly jobs: ScheduledJob[]; } { const jobs: ScheduledJob[] = []; return { register: (job: ScheduledJob) => { jobs.push(job); }, jobs, }; } function createFakeEvents(): EventsEmitter & { readonly emitted: unknown[] } { const emitted: unknown[] = []; return { emitted, emit: (event) => { emitted.push(event); }, }; } function createExtension( id: string, opts: { readonly dependsOn?: readonly string[]; readonly apiVersion?: string; readonly activate?: (host: HostAPI) => void | Promise; readonly deactivate?: () => void | Promise; readonly contributes?: ManifestContributions; } = {}, ): Extension { const base: Manifest = { id, name: id, version: "1.0.0", apiVersion: opts.apiVersion ?? `^${KERNEL_API_VERSION}`, trust: "bundled", }; const manifest: Manifest = opts.dependsOn !== undefined ? { ...base, dependsOn: opts.dependsOn } : opts.contributes !== undefined ? { ...base, contributes: opts.contributes } : base; const ext: Extension = { manifest, activate: opts.activate ?? (() => {}), }; if (opts.deactivate !== undefined) { return { ...ext, deactivate: opts.deactivate }; } return ext; } function createFakeTool(name: string): ToolContract { return { name, description: `Tool ${name}`, parameters: { type: "object" }, execute: async () => ({ content: "ok" }), }; } function createFakeProvider(id: string): ProviderContract { return { id, stream: async function* () {}, }; } function createFakeAuth(id: string): AuthContract { return { id, resolve: async () => null, }; } describe("createHost", () => { let logger: FakeLogger; let logSink: ReturnType; let logDeps: LogDeps; let deps: HostDeps; let scheduler: ReturnType; let events: ReturnType; beforeEach(() => { logger = createFakeLogger(); logSink = createFakeLogSink(); logDeps = createFakeLogDeps(); scheduler = createFakeScheduler(); events = createFakeEvents(); deps = { logger, config: createFakeConfig(), storageFactory: createFakeStorageFactory(), secrets: createFakeSecrets(), permissions: createFakePermissions(), scheduler, bus: createBus(logger), events, logSink, logDeps, }; }); describe("activation order", () => { it("activates extensions in topological order", async () => { const order: string[] = []; const a = createExtension("a", { activate: () => { order.push("a"); }, }); const b = createExtension("b", { dependsOn: ["a"], activate: () => { order.push("b"); }, }); const c = createExtension("c", { dependsOn: ["b"], activate: () => { order.push("c"); }, }); const host = createHost([c, b, a], deps); await host.activate(); expect(order).toEqual(["a", "b", "c"]); }); it("activates independent extensions", async () => { const order: string[] = []; const a = createExtension("a", { activate: () => { order.push("a"); }, }); const b = createExtension("b", { activate: () => { order.push("b"); }, }); const host = createHost([a, b], deps); await host.activate(); expect(order).toHaveLength(2); expect(order).toContain("a"); expect(order).toContain("b"); }); }); describe("fault isolation", () => { it("a throwing extension is isolated — others still activate", async () => { const order: string[] = []; const a = createExtension("a", { activate: () => { order.push("a"); }, }); const b = createExtension("b", { activate: () => { throw new Error("boom"); }, }); const c = createExtension("c", { activate: () => { order.push("c"); }, }); const host = createHost([a, b, c], deps); await host.activate(); expect(order).toEqual(["a", "c"]); expect(host.getDisabled()).toHaveLength(1); expect(host.getDisabled()[0]?.manifest.id).toBe("b"); expect(host.getDisabled()[0]?.reason).toContain("boom"); }); it("an async-rejecting extension is isolated", async () => { const a = createExtension("a", { activate: async () => { throw new Error("async fail"); }, }); const b = createExtension("b", { activate: () => {}, }); const host = createHost([a, b], deps); await host.activate(); expect(host.getDisabled()).toHaveLength(1); expect(host.getDisabled()[0]?.manifest.id).toBe("a"); }); }); describe("apiVersion compatibility", () => { it("activates compatible extensions", async () => { const ext = createExtension("good", { apiVersion: `^${KERNEL_API_VERSION}`, }); const host = createHost([ext], deps); await host.activate(); expect(host.getDisabled()).toHaveLength(0); }); it("disables incompatible extensions without crashing", async () => { const ext = createExtension("bad", { apiVersion: "^99.0.0", }); const host = createHost([ext], deps); await host.activate(); expect(host.getDisabled()).toHaveLength(1); expect(host.getDisabled()[0]?.manifest.id).toBe("bad"); expect(host.getDisabled()[0]?.reason).toContain("incompatible"); }); it("logs a warning for disabled extensions", async () => { const ext = createExtension("bad", { apiVersion: "^99.0.0", }); const host = createHost([ext], deps); await host.activate(); const warnings = logger.logs.filter((l) => l.level === "warn"); expect(warnings).toHaveLength(1); expect(warnings[0]?.message).toContain("bad"); }); }); describe("registries", () => { it("defineTool populates the tool registry", async () => { const tool = createFakeTool("read-file"); const ext = createExtension("tools-fs", { activate: (host) => { host.defineTool(tool); }, }); const host = createHost([ext], deps); await host.activate(); expect(host.getTools().size).toBe(1); expect(host.getTool("read-file")).toBe(tool); }); it("defineProvider populates the provider registry", async () => { const provider = createFakeProvider("anthropic"); const ext = createExtension("provider-anthropic", { activate: (host) => { host.defineProvider(provider); }, }); const host = createHost([ext], deps); await host.activate(); expect(host.getProviders().size).toBe(1); expect(host.getProvider("anthropic")).toBe(provider); }); it("defineAuth populates the auth registry", async () => { const auth = createFakeAuth("apikey"); const ext = createExtension("auth-apikey", { activate: (host) => { host.defineAuth(auth); }, }); const host = createHost([ext], deps); await host.activate(); expect(host.getAuthProviders().size).toBe(1); expect(host.getAuthProvider("apikey")).toBe(auth); }); it("getService returns what an extension provided via provideService", async () => { const handle = defineService<{ value: number }>("test/svc"); const ext = createExtension("svc-provider", { activate: (host) => { host.provideService(handle, { value: 42 }); }, }); const consumer = createExtension("svc-consumer", { dependsOn: ["svc-provider"], activate: (host) => { const svc = host.getService(handle); expect(svc.value).toBe(42); }, }); const host = createHost([ext, consumer], deps); await host.activate(); expect(host.getDisabled()).toHaveLength(0); }); it("multiple extensions contribute to the same registry", async () => { const ext1 = createExtension("tools-a", { activate: (host) => { host.defineTool(createFakeTool("tool-a")); }, }); const ext2 = createExtension("tools-b", { activate: (host) => { host.defineTool(createFakeTool("tool-b")); }, }); const host = createHost([ext1, ext2], deps); await host.activate(); expect(host.getTools().size).toBe(2); expect(host.getTool("tool-a")).toBeDefined(); expect(host.getTool("tool-b")).toBeDefined(); }); }); describe("scheduler", () => { it("collects scheduled jobs and forwards to sink", async () => { const job: ScheduledJob = { id: "cache-warm", cron: "*/5 * * * *", execute: () => {}, }; const ext = createExtension("scheduler-ext", { activate: (host) => { host.scheduler.register(job); }, }); const host = createHost([ext], deps); await host.activate(); expect(host.getScheduledJobs()).toHaveLength(1); expect(host.getScheduledJobs()[0]).toBe(job); expect(scheduler.jobs).toHaveLength(1); expect(scheduler.jobs[0]).toBe(job); }); }); describe("migrations", () => { it("collects migrations from manifests", async () => { const ext = createExtension("store-ext", { contributes: { migrations: ["001-init", "002-add-index"] }, }); const host = createHost([ext], deps); await host.activate(); expect(host.getMigrations()).toEqual(["001-init", "002-add-index"]); }); }); describe("deactivation", () => { it("deactivates in reverse activation order", async () => { const order: string[] = []; const a = createExtension("a", { activate: () => { order.push("activate-a"); }, deactivate: () => { order.push("deactivate-a"); }, }); const b = createExtension("b", { activate: () => { order.push("activate-b"); }, deactivate: () => { order.push("deactivate-b"); }, }); const c = createExtension("c", { activate: () => { order.push("activate-c"); }, deactivate: () => { order.push("deactivate-c"); }, }); const host = createHost([a, b, c], deps); await host.activate(); await host.deactivate(); expect(order).toEqual([ "activate-a", "activate-b", "activate-c", "deactivate-c", "deactivate-b", "deactivate-a", ]); }); it("a failing deactivate does not prevent others", async () => { const order: string[] = []; const a = createExtension("a", { activate: () => {}, deactivate: () => { order.push("deactivate-a"); }, }); const b = createExtension("b", { activate: () => {}, deactivate: () => { throw new Error("deactivate boom"); }, }); const c = createExtension("c", { activate: () => {}, deactivate: () => { order.push("deactivate-c"); }, }); const host = createHost([a, b, c], deps); await host.activate(); await host.deactivate(); expect(order).toEqual(["deactivate-c", "deactivate-a"]); const errors = logger.logs.filter((l) => l.level === "error"); expect(errors.some((e) => e.message.includes("deactivate"))).toBe(true); expect(errors.some((e) => (e.attrs as { err?: unknown })?.err instanceof Error)).toBe(true); }); }); describe("HostAPI delegation", () => { it("on/addFilter delegate to the bus", async () => { const hook = defineEventHook("test/host-event"); const received: string[] = []; const ext = createExtension("hook-ext", { activate: (host) => { host.on(hook, (payload) => { received.push(payload); }); }, }); const host = createHost([ext], deps); await host.activate(); deps.bus.emit(hook, "hello"); expect(received).toEqual(["hello"]); }); it("emit dispatches to handlers registered via on", async () => { const hook = defineEventHook("test/emit-dispatch"); const received: string[] = []; const ext = createExtension("emit-ext", { activate: (host) => { host.on(hook, (payload) => { received.push(payload); }); }, }); const host = createHost([ext], deps); await host.activate(); const api = host.getHostAPI(); api.emit(hook, "world"); expect(received).toEqual(["world"]); }); it("emit isolates a throwing handler (does not propagate)", async () => { const hook = defineEventHook("test/emit-isolation"); const received: string[] = []; const ext = createExtension("emit-isolation-ext", { activate: (host) => { host.on(hook, () => { throw new Error("handler boom"); }); host.on(hook, (payload) => { received.push(payload); }); }, }); const host = createHost([ext], deps); await host.activate(); const api = host.getHostAPI(); expect(() => api.emit(hook, "safe")).not.toThrow(); expect(received).toEqual(["safe"]); }); it("applyFilters threads a value through registered filters in order", async () => { const hook = defineFilter("test/text-transform"); const ext = createExtension("filter-ext", { activate: (host) => { host.addFilter(hook, (value) => `${value}-first`); host.addFilter(hook, (value) => `${value}-second`); }, }); const host = createHost([ext], deps); await host.activate(); const api = host.getHostAPI(); const result = await api.applyFilters(hook, "start"); expect(result).toBe("start-first-second"); }); it("applyFilters returns the input unchanged when no filters are registered", async () => { const hook = defineFilter("test/unused-filter"); const ext = createExtension("no-filter-ext", { activate: () => {}, }); const host = createHost([ext], deps); await host.activate(); const api = host.getHostAPI(); const result = await api.applyFilters(hook, "unchanged"); expect(result).toBe("unchanged"); }); it("storage delegates to the factory", async () => { let storageResult: StorageNamespace | undefined; const ext = createExtension("storage-ext", { activate: (host) => { storageResult = host.storage("my-ns"); }, }); const host = createHost([ext], deps); await host.activate(); expect(storageResult).toBeDefined(); await storageResult?.set("key", "value"); expect(await storageResult?.get("key")).toBe("value"); }); it("events delegates to the emitter", async () => { const ext = createExtension("event-ext", { activate: (host) => { host.events.emit({ type: "custom", data: 42 }); }, }); const host = createHost([ext], deps); await host.activate(); expect(events.emitted).toHaveLength(1); expect(events.emitted[0]).toEqual({ type: "custom", data: 42 }); }); }); describe("HostAPI registry access", () => { it("getTools returns registered tools via HostAPI", async () => { const tool = createFakeTool("read-file"); let capturedTools: ReadonlyMap | undefined; const producer = createExtension("tools-fs", { activate: (host) => { host.defineTool(tool); }, }); const consumer = createExtension("consumer", { dependsOn: ["tools-fs"], activate: (host) => { capturedTools = host.getTools(); }, }); const host = createHost([producer, consumer], deps); await host.activate(); expect(capturedTools).toBeDefined(); expect(capturedTools?.size).toBe(1); expect(capturedTools?.get("read-file")).toBe(tool); }); it("getProviders returns registered providers via HostAPI", async () => { const provider = createFakeProvider("anthropic"); let capturedProviders: ReadonlyMap | undefined; const producer = createExtension("provider-anthropic", { activate: (host) => { host.defineProvider(provider); }, }); const consumer = createExtension("consumer", { dependsOn: ["provider-anthropic"], activate: (host) => { capturedProviders = host.getProviders(); }, }); const host = createHost([producer, consumer], deps); await host.activate(); expect(capturedProviders).toBeDefined(); expect(capturedProviders?.size).toBe(1); expect(capturedProviders?.get("anthropic")).toBe(provider); }); it("getAuthProviders/getAuthProvider returns registered auth via HostAPI", async () => { const auth = createFakeAuth("apikey"); let capturedAuth: ReadonlyMap | undefined; let capturedSingle: AuthContract | undefined; const producer = createExtension("auth-apikey", { activate: (host) => { host.defineAuth(auth); }, }); const consumer = createExtension("consumer", { dependsOn: ["auth-apikey"], activate: (host) => { capturedAuth = host.getAuthProviders(); capturedSingle = host.getAuthProvider("apikey"); }, }); const host = createHost([producer, consumer], deps); await host.activate(); expect(capturedAuth).toBeDefined(); expect(capturedAuth?.size).toBe(1); expect(capturedAuth?.get("apikey")).toBe(auth); expect(capturedSingle).toBe(auth); }); }); describe("getExtensions", () => { it("returns empty array when no extensions are activated", async () => { const host = createHost([], deps); await host.activate(); expect(host.getExtensions()).toEqual([]); }); it("returns manifests of all activated extensions", async () => { const a = createExtension("ext-a"); const b = createExtension("ext-b"); const host = createHost([a, b], deps); await host.activate(); const exts = host.getExtensions(); expect(exts).toHaveLength(2); expect(exts.map((e) => e.id)).toContain("ext-a"); expect(exts.map((e) => e.id)).toContain("ext-b"); }); it("returns manifests in activation order", async () => { const a = createExtension("a"); const b = createExtension("b", { dependsOn: ["a"] }); const c = createExtension("c", { dependsOn: ["b"] }); const host = createHost([c, b, a], deps); await host.activate(); const exts = host.getExtensions(); expect(exts.map((e) => e.id)).toEqual(["a", "b", "c"]); }); it("excludes extensions that failed to activate", async () => { const a = createExtension("good"); const b = createExtension("bad", { activate: () => { throw new Error("boom"); }, }); const host = createHost([a, b], deps); await host.activate(); const exts = host.getExtensions(); expect(exts).toHaveLength(1); expect(exts[0]?.id).toBe("good"); }); it("excludes extensions disabled by apiVersion incompatibility", async () => { const good = createExtension("good"); const bad = createExtension("bad", { apiVersion: "^99.0.0" }); const host = createHost([good, bad], deps); await host.activate(); const exts = host.getExtensions(); expect(exts).toHaveLength(1); expect(exts[0]?.id).toBe("good"); }); it("returns a frozen array", async () => { const ext = createExtension("ext"); const host = createHost([ext], deps); await host.activate(); const exts = host.getExtensions(); expect(Object.isFrozen(exts)).toBe(true); }); it("HostAPI getExtensions reflects activated extensions after full activation", async () => { const a = createExtension("ext-a"); const b = createExtension("ext-b", { dependsOn: ["ext-a"], activate: () => {}, }); const host = createHost([a, b], deps); await host.activate(); // Use getHostAPI() to verify the post-activation view const api = host.getHostAPI(); const capturedExtsAfter = api.getExtensions(); expect(capturedExtsAfter).toHaveLength(2); expect(capturedExtsAfter.map((e) => e.id)).toEqual(["ext-a", "ext-b"]); }); it("HostAPI getExtensions during activation sees only previously activated", async () => { const seenDuringActivation: string[][] = []; const a = createExtension("a", { activate: (host) => { seenDuringActivation.push(host.getExtensions().map((e) => e.id)); }, }); const b = createExtension("b", { activate: (host) => { seenDuringActivation.push(host.getExtensions().map((e) => e.id)); }, }); const host = createHost([a, b], deps); await host.activate(); // When a activates, activated[] is empty (a hasn't been pushed yet) // When b activates, activated[] has [a] (b hasn't been pushed yet) expect(seenDuringActivation).toEqual([[], ["a"]]); }); }); describe("DAG errors", () => { it("throws on missing dependency", () => { const ext = createExtension("a", { dependsOn: ["missing"] }); expect(() => createHost([ext], deps)).toThrow(/not available/); }); it("throws on dependency cycle", () => { const a = createExtension("a", { dependsOn: ["b"] }); const b = createExtension("b", { dependsOn: ["a"] }); expect(() => createHost([a, b], deps)).toThrow(/cycle/i); }); }); describe("empty host", () => { it("works with no extensions", async () => { const host = createHost([], deps); await host.activate(); expect(host.getTools().size).toBe(0); expect(host.getProviders().size).toBe(0); expect(host.getAuthProviders().size).toBe(0); expect(host.getScheduledJobs()).toHaveLength(0); expect(host.getMigrations()).toHaveLength(0); expect(host.getDisabled()).toHaveLength(0); }); }); describe("getHostAPI", () => { it("returns a HostAPI whose read-views reflect registrations from activation", async () => { const tool = createFakeTool("read-file"); const provider = createFakeProvider("anthropic"); const auth = createFakeAuth("apikey"); const ext = createExtension("multi-ext", { activate: (host) => { host.defineTool(tool); host.defineProvider(provider); host.defineAuth(auth); }, }); const host = createHost([ext], deps); await host.activate(); const api = host.getHostAPI(); expect(api.getTools().size).toBe(1); expect(api.getTools().get("read-file")).toBe(tool); expect(api.getProviders().size).toBe(1); expect(api.getProviders().get("anthropic")).toBe(provider); expect(api.getAuthProviders().size).toBe(1); expect(api.getAuthProvider("apikey")).toBe(auth); }); it("throws on defineTool after activation", async () => { const ext = createExtension("ext", { activate: () => {} }); const host = createHost([ext], deps); await host.activate(); const api = host.getHostAPI(); expect(() => api.defineTool(createFakeTool("late"))).toThrow( "Registration not available after activation", ); }); it("throws on defineProvider after activation", async () => { const ext = createExtension("ext", { activate: () => {} }); const host = createHost([ext], deps); await host.activate(); const api = host.getHostAPI(); expect(() => api.defineProvider(createFakeProvider("late"))).toThrow( "Registration not available after activation", ); }); it("throws on defineAuth after activation", async () => { const ext = createExtension("ext", { activate: () => {} }); const host = createHost([ext], deps); await host.activate(); const api = host.getHostAPI(); expect(() => api.defineAuth(createFakeAuth("late"))).toThrow( "Registration not available after activation", ); }); it("applyFilters is available on registration-closed HostAPI", async () => { const hook = defineFilter("test/closed-filter"); const ext = createExtension("filter-ext", { activate: (host) => { host.addFilter(hook, (value) => `${value}-filtered`); }, }); const host = createHost([ext], deps); await host.activate(); const api = host.getHostAPI(); const result = await api.applyFilters(hook, "input"); expect(result).toBe("input-filtered"); }); }); describe("auto-scoped logger (D6)", () => { it("each extension's logger stamps its own manifest.id as extensionId", async () => { let extALogger: Logger | undefined; let extBLogger: Logger | undefined; const a = createExtension("ext-a", { activate: (host) => { extALogger = host.logger; }, }); const b = createExtension("ext-b", { activate: (host) => { extBLogger = host.logger; }, }); const host = createHost([a, b], deps); await host.activate(); extALogger?.info("from-a"); extBLogger?.info("from-b"); const logRecords = logSink.records.filter((r) => r.kind === "log"); expect(logRecords).toHaveLength(2); if (logRecords[0]?.kind === "log") { expect(logRecords[0].extensionId).toBe("ext-a"); expect(logRecords[0].msg).toBe("from-a"); } if (logRecords[1]?.kind === "log") { expect(logRecords[1].extensionId).toBe("ext-b"); expect(logRecords[1].msg).toBe("from-b"); } }); it("an extension cannot spoof extensionId — it is auto-stamped", async () => { let extLogger: Logger | undefined; const ext = createExtension("real-id", { activate: (host) => { extLogger = host.logger; }, }); const host = createHost([ext], deps); await host.activate(); // child() cannot override extensionId const child = extLogger?.child({ extensionId: "spoofed" }); child?.info("msg"); const logRecords = logSink.records.filter((r) => r.kind === "log"); expect(logRecords).toHaveLength(1); if (logRecords[0]?.kind === "log") { expect(logRecords[0].extensionId).toBe("real-id"); } }); it("host.logger.error uses structured { err } shape", async () => { let extLogger: Logger | undefined; const ext = createExtension("ext", { activate: (host) => { extLogger = host.logger; }, }); const host = createHost([ext], deps); await host.activate(); extLogger?.error("something broke", { err: new Error("boom") }); const logRecords = logSink.records.filter((r) => r.kind === "log"); expect(logRecords).toHaveLength(1); if (logRecords[0]?.kind === "log") { expect(logRecords[0].level).toBe("error"); expect(logRecords[0].msg).toBe("something broke"); expect(logRecords[0].attributes?.["error.message"]).toBe("boom"); } }); it("a throwing sink does NOT break the caller", async () => { const brokenSink: LogSink = { emit() { throw new Error("sink down"); }, }; const brokenDeps: HostDeps = { ...deps, logSink: brokenSink, }; let extLogger: Logger | undefined; const ext = createExtension("ext", { activate: (host) => { extLogger = host.logger; }, }); const host = createHost([ext], brokenDeps); await host.activate(); // Should not throw expect(() => extLogger?.info("msg")).not.toThrow(); }); it("span() + end() emit incremental span-open and span-close records", async () => { let extLogger: Logger | undefined; const ext = createExtension("ext", { activate: (host) => { extLogger = host.logger; }, }); const host = createHost([ext], deps); await host.activate(); const span = extLogger?.span("my-span", { key: "value" }); span?.setAttributes({ extra: "attr" }); span?.end({ attrs: { result: "ok" } }); const spanOpens = logSink.records.filter((r) => r.kind === "span-open"); const spanCloses = logSink.records.filter((r) => r.kind === "span-close"); expect(spanOpens).toHaveLength(1); expect(spanCloses).toHaveLength(1); if (spanOpens[0]?.kind === "span-open") { expect(spanOpens[0].name).toBe("my-span"); expect(spanOpens[0].extensionId).toBe("ext"); expect(spanOpens[0].attributes?.key).toBe("value"); } if (spanCloses[0]?.kind === "span-close") { expect(spanCloses[0].name).toBe("my-span"); expect(spanCloses[0].status).toBe("ok"); expect(spanCloses[0].durationMs).toBeGreaterThanOrEqual(0); expect(spanCloses[0].attributes?.extra).toBe("attr"); expect(spanCloses[0].attributes?.result).toBe("ok"); } }); it("span() with body emits body on span-open record", async () => { let extLogger: Logger | undefined; const ext = createExtension("ext", { activate: (host) => { extLogger = host.logger; }, }); const host = createHost([ext], deps); await host.activate(); const span = extLogger?.span("with-body", { key: "value" }, '{"payload":"hello"}'); span?.end(); const spanOpens = logSink.records.filter((r) => r.kind === "span-open"); expect(spanOpens).toHaveLength(1); if (spanOpens[0]?.kind === "span-open") { expect(spanOpens[0].body).toBe('{"payload":"hello"}'); } }); it("span() without body omits body field on span-open record", async () => { let extLogger: Logger | undefined; const ext = createExtension("ext", { activate: (host) => { extLogger = host.logger; }, }); const host = createHost([ext], deps); await host.activate(); const span = extLogger?.span("no-body"); span?.end(); const spanOpens = logSink.records.filter((r) => r.kind === "span-open"); expect(spanOpens).toHaveLength(1); if (spanOpens[0]?.kind === "span-open") { expect(spanOpens[0].body).toBeUndefined(); } }); it("child() with body emits body on child span-open record", async () => { let extLogger: Logger | undefined; const ext = createExtension("ext", { activate: (host) => { extLogger = host.logger; }, }); const host = createHost([ext], deps); await host.activate(); const span = extLogger?.span("parent"); const child = span?.child("child-name", { k: "v" }, '{"child":"body"}'); child?.end(); span?.end(); const spanOpens = logSink.records.filter((r) => r.kind === "span-open"); const childOpen = spanOpens.find((r) => r.kind === "span-open" && r.name === "child-name"); expect(childOpen).toBeDefined(); if (childOpen?.kind === "span-open") { expect(childOpen.body).toBe('{"child":"body"}'); } }); it("end() with body emits body on span-close record", async () => { let extLogger: Logger | undefined; const ext = createExtension("ext", { activate: (host) => { extLogger = host.logger; }, }); const host = createHost([ext], deps); await host.activate(); const span = extLogger?.span("close-body"); span?.end({ body: '{"result":"data"}' }); const spanCloses = logSink.records.filter((r) => r.kind === "span-close"); expect(spanCloses).toHaveLength(1); if (spanCloses[0]?.kind === "span-close") { expect(spanCloses[0].body).toBe('{"result":"data"}'); } }); it("end() without body omits body field on span-close record", async () => { let extLogger: Logger | undefined; const ext = createExtension("ext", { activate: (host) => { extLogger = host.logger; }, }); const host = createHost([ext], deps); await host.activate(); const span = extLogger?.span("no-close-body"); span?.end(); const spanCloses = logSink.records.filter((r) => r.kind === "span-close"); expect(spanCloses).toHaveLength(1); if (spanCloses[0]?.kind === "span-close") { expect(spanCloses[0].body).toBeUndefined(); } }); }); });