import { describe, expect, test } from "bun:test"; import { EventEmitter } from "node:events"; import type { ExtensionAPI, ExtensionContext, } from "@earendil-works/pi-coding-agent"; import { getKnowledgeHierarchyClient, getKnowledgeRootRuntime, type KnowledgeHierarchyClient, type KnowledgeRootRuntime, readKnowledgeBlock, registerKnowledgeContextRuntime, registerKnowledgeHierarchyClient, registerKnowledgeRootRuntime, } from "./knowledge-runtime"; /** Creates one extension API carrier without loading unrelated extension behavior. */ function createPi(): ExtensionAPI { return { events: new EventEmitter() } as unknown as ExtensionAPI; } describe("knowledge runtime registry", () => { /** * Proves explicit model-request builders see one process-local knowledge source when registered. * Inputs and expected outputs: absence returns null, registration returns the source block, and disposal restores absence. * Edge case: disposal cannot clear a newer replacement registration. * Dependencies: one EventEmitter acts as Pi's process-local cross-extension carrier. */ test("publishes and disposes the context source without fallback", async () => { // Arrange: no source exists before the knowledge extension loads. const pi = createPi(); const ctx = {} as ExtensionContext; const first = { readBlock: async () => "first" }; const second = { readBlock: async () => "second" }; // Act: replace the source, dispose the stale registration, then dispose the owner. expect(await readKnowledgeBlock(pi, ctx)).toBeNull(); const disposeFirst = registerKnowledgeContextRuntime(pi, first); expect(await readKnowledgeBlock(pi, ctx)).toBe( "first", ); const disposeSecond = registerKnowledgeContextRuntime(pi, second); disposeFirst(); // Assert: stale disposal preserves the replacement and owner disposal removes it. expect(await readKnowledgeBlock(pi, ctx)).toBe( "second", ); disposeSecond(); expect(await readKnowledgeBlock(pi, ctx)).toBeNull(); }); /** * Proves root and child hierarchy contracts use separate narrow registry slots. * Inputs and expected outputs: both exact objects are retrievable and independently disposable. * Edge case: removing the child client leaves the root coordinator available. * Dependencies: transport behavior is tested separately by run-subagent bridge tests. */ test("keeps root coordination and child transport ownership separate", () => { // Arrange: minimal typed objects expose no generic message broker surface. const pi = createPi(); const root = {} as KnowledgeRootRuntime; const child = {} as KnowledgeHierarchyClient; // Act: register both process-local roles. const disposeRoot = registerKnowledgeRootRuntime(pi, root); const disposeChild = registerKnowledgeHierarchyClient(pi, child); // Assert: each slot preserves exact ownership and disposal semantics. expect(getKnowledgeRootRuntime(pi)).toBe(root); expect(getKnowledgeHierarchyClient(pi)).toBe(child); disposeChild(); expect(getKnowledgeHierarchyClient(pi)).toBeUndefined(); expect(getKnowledgeRootRuntime(pi)).toBe(root); disposeRoot(); expect(getKnowledgeRootRuntime(pi)).toBeUndefined(); }); });