import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { executeMemoryManager, executeSkillManager, } from "../../src/learning/toolManagers.js"; import { MemoryStore } from "../../src/memory/store.js"; import { getRuntimeSessionContext, removeRuntimeSessionContext, setRuntimeSessionContext, type RuntimeSessionContext, } from "../../src/runtime/sessionContext.js"; import { SkillStore } from "../../src/skills/store.js"; describe("main-process learning tool managers", () => { let tempDir: string; let memoryStore: MemoryStore; let skillStore: SkillStore; let context: RuntimeSessionContext; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "tjwater-learning-tools-")); memoryStore = new MemoryStore( join(tempDir, "memory"), join(tempDir, "backup", "memory"), ); skillStore = new SkillStore( join(tempDir, "skills"), join(tempDir, "backup", "skills"), ); await memoryStore.initialize(); context = { actorKey: "actor-1", allowLearningWrite: true, clientSessionId: "client-session-1", projectKey: "project-1", sessionId: "session-1", traceId: "trace-1", }; setRuntimeSessionContext(context); }); afterEach(async () => { removeRuntimeSessionContext(context.sessionId); await rm(tempDir, { force: true, recursive: true }); }); it("enforces list-before-add using the canonical runtime context", async () => { const rejected = await executeMemoryManager(memoryStore, context, { action: "add", content: "用户偏好查看压力单位为 MPa", scope: "user", }); expect(rejected.decision).toBe("rejected"); await executeMemoryManager(memoryStore, context, { action: "list", scope: "user", }); const refreshedContext = getRuntimeSessionContext(context.sessionId)!; const accepted = await executeMemoryManager(memoryStore, refreshedContext, { action: "add", content: "用户偏好查看压力单位为 MPa", scope: "user", }); expect(accepted.decision).toBe("accepted"); expect(await memoryStore.list("user", context.actorKey)).toHaveLength(1); }); it("writes and removes skills through the shared store", async () => { const content = [ "---", "name: pressure-review", "description: Pressure review workflow.", "---", "", "# Pressure Review", ].join("\n"); const written = await executeSkillManager(skillStore, context, { action: "write_skill", content, skill_path: "workflow/pressure-review", }); expect(written.decision).toBe("accepted"); expect("target" in written).toBe(true); if (!("target" in written)) throw new Error("write returned no target"); await expect(readFile(written.target, "utf8")).resolves.toContain( "# Pressure Review\n", ); const removed = await executeSkillManager(skillStore, context, { action: "remove_skill", skill_path: "workflow/pressure-review", }); expect(removed.decision).toBe("accepted"); expect("target" in removed).toBe(true); if (!("target" in removed)) throw new Error("remove returned no target"); await expect(readFile(removed.target, "utf8")).rejects.toThrow(); }); });