import { afterEach, describe, it } from "node:test"; import assert from "node:assert/strict"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import hermesMemoryChildSearch from "../src/child-search.js"; import { CHILD_SEARCH_EXTENSION_PATH, registerSubagentMemorySearch } from "../src/index.js"; import { loadConfig } from "../src/config.js"; import { resolveMemoryRoots } from "../src/paths.js"; import { DatabaseManager } from "../src/store/db.js"; import { addMemory, getMemoryUsageSignals } from "../src/store/sqlite-memory-store.js"; let root = ""; afterEach(() => { if (root) fs.rmSync(root, { recursive: true, force: true }); root = ""; }); function childHost() { const tools = new Map(); const handlers = new Map Promise>>(); const listeners = new Map void>>(); const pi = { registerTool: (tool: any) => tools.set(tool.name, tool), on: (event: string, handler: () => Promise) => handlers.set(event, [...(handlers.get(event) ?? []), handler]), events: { on: (channel: string, handler: (data: unknown) => void) => listeners.set(channel, [...(listeners.get(channel) ?? []), handler]), emit: (channel: string, data: unknown) => { for (const handler of listeners.get(channel) ?? []) handler(data); }, }, }; return { pi, tools, handlers }; } const defaults = () => loadConfig(path.join(root, "missing-config.json"), path.join(root, "missing-settings.json")); describe("memory search for subagent children", () => { it("asks pi-subagents to load only the read-only child entrypoint", () => { const { pi } = childHost(); registerSubagentMemorySearch(pi as any); const registered: unknown[] = []; pi.events.emit("pi-subagents:request-builtin-agent-augmentations", { register: (augmentation: unknown) => registered.push(augmentation) }); assert.deepEqual(registered, [{ id: "pi-hermes-memory", agentNames: ["scout", "reviewer", "worker", "delegate", "oracle"], addTools: ["memory_search", "session_search"], subagentOnlyExtensions: [CHILD_SEARCH_EXTENSION_PATH], }]); assert.ok(fs.existsSync(CHILD_SEARCH_EXTENSION_PATH), CHILD_SEARCH_EXTENSION_PATH); assert.doesNotThrow(() => pi.events.emit("pi-subagents:request-builtin-agent-augmentations", undefined)); }); it("registers only the two search tools and one shutdown handler, and records no usage hits", async () => { root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-memory-child-search-")); const memoryDir = path.join(root, "memory-store"); const parentDb = new DatabaseManager(memoryDir); addMemory(parentDb, "child visible deployment convention", "memory"); parentDb.close(); const { pi, tools, handlers } = childHost(); hermesMemoryChildSearch(pi as any, { config: { ...defaults(), memoryDir }, agentRoot: root }); assert.deepEqual([...tools.keys()].sort(), ["memory_search", "session_search"]); assert.deepEqual([...handlers.keys()], ["session_shutdown"]); const result = await tools.get("memory_search").execute("tc-1", { query: "deployment convention" }); assert.equal(result.details.success, true); assert.match(result.content[0].text, /child visible deployment convention/); for (const handler of handlers.get("session_shutdown") ?? []) await handler(); const check = new DatabaseManager(memoryDir); assert.equal(getMemoryUsageSignals(check, { target: "memory", project: null }).size, 0); check.close(); }); it("refuses to open the database while the parent's legacy migration is pending", async () => { root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-memory-child-migration-")); const { legacyGlobalDir, globalDir } = resolveMemoryRoots(undefined, root); fs.mkdirSync(legacyGlobalDir, { recursive: true }); fs.writeFileSync(path.join(legacyGlobalDir, "sessions.db"), ""); const { pi, tools } = childHost(); hermesMemoryChildSearch(pi as any, { config: defaults(), agentRoot: root }); await assert.rejects(tools.get("memory_search").execute("tc-1", { query: "anything" }), /migration is pending/); assert.equal(fs.existsSync(path.join(globalDir, "sessions.db")), false); }); }); describe("resolveMemoryRoots", () => { it("uses the extension root by default and for the legacy dir, and a custom dir as given", () => { const agentRoot = path.join(os.tmpdir(), "agent-root"); const extensionRoot = path.join(agentRoot, "pi-hermes-memory"); assert.deepEqual(resolveMemoryRoots(undefined, agentRoot), { legacyGlobalDir: path.join(agentRoot, "memory"), globalDir: extensionRoot, shouldMigrateExtensionRoot: true }); assert.equal(resolveMemoryRoots(path.join(agentRoot, "memory"), agentRoot).globalDir, extensionRoot); assert.deepEqual(resolveMemoryRoots("/custom/memory", agentRoot), { legacyGlobalDir: path.join(agentRoot, "memory"), globalDir: "/custom/memory", shouldMigrateExtensionRoot: false }); }); });