diff --git a/packages/cli/src/__tests__/project-context.test.ts b/packages/cli/src/__tests__/project-context.test.ts index ea87496ab..bad85ef47 100644 --- a/packages/cli/src/__tests__/project-context.test.ts +++ b/packages/cli/src/__tests__/project-context.test.ts @@ -22,6 +22,7 @@ describe("project-context", () => { let homeDir: string; let central: CentralCore; const originalHome = process.env.HOME; + const createdProjectIds: string[] = []; beforeEach(async () => { tempDir = mkdtempSync(join(tmpdir(), "kb-test-")); @@ -32,8 +33,26 @@ describe("project-context", () => { }); afterEach(async () => { - await central.close(); + // Teardown order: entity cleanup first, then infrastructure, then filesystem + // Unregister all tracked projects first + for (const projectId of createdProjectIds) { + try { + await central.unregisterProject(projectId); + } catch { + // Ignore cleanup errors for already-removed entities + } + } + createdProjectIds.length = 0; + + // Close CentralCore before filesystem cleanup + try { + await central.close(); + } catch { + // Ignore close errors + } clearStoreCache(); + + // Filesystem cleanup last try { rmSync(tempDir, { recursive: true, force: true }); rmSync(homeDir, { recursive: true, force: true }); @@ -61,6 +80,7 @@ describe("project-context", () => { name: "my-project", path: resolve(projectPath), }); + createdProjectIds.push(project.id); const found = await detectProjectFromCwd(projectPath, central); @@ -78,6 +98,7 @@ describe("project-context", () => { name: "my-project", path: resolve(projectPath), }); + createdProjectIds.push(project.id); const found = await detectProjectFromCwd(subDir, central); diff --git a/packages/core/src/__tests__/agent-instructions-bundle.test.ts b/packages/core/src/__tests__/agent-instructions-bundle.test.ts index 6ab6fad77..bc9629cab 100644 --- a/packages/core/src/__tests__/agent-instructions-bundle.test.ts +++ b/packages/core/src/__tests__/agent-instructions-bundle.test.ts @@ -7,6 +7,7 @@ import { AgentStore } from "../agent-store.js"; describe("AgentStore — instructions bundle", () => { let testDir: string; let store: AgentStore; + const createdAgentIds: string[] = []; beforeEach(async () => { testDir = await mkdtemp(join(tmpdir(), "agent-instructions-bundle-test-")); @@ -15,7 +16,23 @@ describe("AgentStore — instructions bundle", () => { }); afterEach(async () => { - await rm(testDir, { recursive: true, force: true }); + // Teardown order: entity cleanup first, then filesystem + // Delete all created agents explicitly + for (const agentId of createdAgentIds) { + try { + await store.deleteAgent(agentId); + } catch { + // Ignore cleanup errors for already-removed entities + } + } + createdAgentIds.length = 0; + + // Filesystem cleanup last + try { + await rm(testDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } }); it("persists bundleConfig through create + load roundtrip", async () => { @@ -28,6 +45,7 @@ describe("AgentStore — instructions bundle", () => { files: ["AGENTS.md", "STYLE.md"], }, }); + createdAgentIds.push(created.id); expect(created.bundleConfig).toEqual({ mode: "managed", @@ -41,11 +59,13 @@ describe("AgentStore — instructions bundle", () => { it("getInstructionsDir returns the managed bundle directory path", async () => { const agent = await store.createAgent({ name: "dir-agent", role: "executor" }); + createdAgentIds.push(agent.id); expect(store.getInstructionsDir(agent.id)).toBe(join(testDir, "agents", `${agent.id}-instructions`)); }); it("listBundleFiles returns empty for missing directory and sorted .md files only", async () => { const agent = await store.createAgent({ name: "list-agent", role: "executor" }); + createdAgentIds.push(agent.id); expect(await store.listBundleFiles(agent.id)).toEqual([]); @@ -61,6 +81,7 @@ describe("AgentStore — instructions bundle", () => { it("readBundleFile reads content and rejects missing/traversal paths", async () => { const agent = await store.createAgent({ name: "read-agent", role: "executor" }); + createdAgentIds.push(agent.id); await store.writeBundleFile(agent.id, "AGENTS.md", "Hello bundle"); await expect(store.readBundleFile(agent.id, "AGENTS.md")).resolves.toBe("Hello bundle"); @@ -71,6 +92,7 @@ describe("AgentStore — instructions bundle", () => { it("writeBundleFile creates directories, overwrites, validates paths, and enforces max file count", async () => { const agent = await store.createAgent({ name: "write-agent", role: "executor" }); + createdAgentIds.push(agent.id); const dir = store.getInstructionsDir(agent.id); await store.writeBundleFile(agent.id, "AGENTS.md", "first"); @@ -93,6 +115,7 @@ describe("AgentStore — instructions bundle", () => { it("deleteBundleFile removes files and throws when missing", async () => { const agent = await store.createAgent({ name: "delete-agent", role: "executor" }); + createdAgentIds.push(agent.id); const filePath = join(store.getInstructionsDir(agent.id), "AGENTS.md"); await store.writeBundleFile(agent.id, "AGENTS.md", "to-delete"); @@ -104,6 +127,7 @@ describe("AgentStore — instructions bundle", () => { it("setBundleConfig validates input and creates managed directory", async () => { const agent = await store.createAgent({ name: "config-agent", role: "executor" }); + createdAgentIds.push(agent.id); const managed = await store.setBundleConfig(agent.id, { mode: "managed", @@ -143,6 +167,7 @@ describe("AgentStore — instructions bundle", () => { role: "executor", instructionsText: "Legacy text content", }); + createdAgentIds.push(agent.id); const migrated = await store.migrateLegacyInstructions(agent.id); @@ -166,6 +191,7 @@ describe("AgentStore — instructions bundle", () => { role: "executor", instructionsPath: sourcePath, }); + createdAgentIds.push(agent.id); const migrated = await store.migrateLegacyInstructions(agent.id); @@ -189,6 +215,7 @@ describe("AgentStore — instructions bundle", () => { instructionsText: "Primary inline content", instructionsPath: sourcePath, }); + createdAgentIds.push(agent.id); const migrated = await store.migrateLegacyInstructions(agent.id); @@ -215,6 +242,7 @@ describe("AgentStore — instructions bundle", () => { }, instructionsText: "should-stay", }); + createdAgentIds.push(agent.id); const migrated = await store.migrateLegacyInstructions(agent.id); @@ -231,6 +259,7 @@ describe("AgentStore — instructions bundle", () => { name: "no-legacy", role: "executor", }); + createdAgentIds.push(agent.id); const migrated = await store.migrateLegacyInstructions(agent.id); diff --git a/packages/core/src/__tests__/agent-instructions.test.ts b/packages/core/src/__tests__/agent-instructions.test.ts index 5240f9e8e..39e29c948 100644 --- a/packages/core/src/__tests__/agent-instructions.test.ts +++ b/packages/core/src/__tests__/agent-instructions.test.ts @@ -7,6 +7,7 @@ import { AgentStore } from "../agent-store.js"; describe("AgentStore — instructions fields", () => { let testDir: string; let store: AgentStore; + const createdAgentIds: string[] = []; beforeEach(async () => { testDir = await mkdtemp(join(tmpdir(), "agent-instructions-test-")); @@ -15,7 +16,23 @@ describe("AgentStore — instructions fields", () => { }); afterEach(async () => { - await rm(testDir, { recursive: true, force: true }); + // Teardown order: entity cleanup first, then filesystem + // Delete all created agents explicitly + for (const agentId of createdAgentIds) { + try { + await store.deleteAgent(agentId); + } catch { + // Ignore cleanup errors for already-removed entities + } + } + createdAgentIds.length = 0; + + // Filesystem cleanup last + try { + await rm(testDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } }); it("creates an agent with instructionsText", async () => { @@ -24,6 +41,7 @@ describe("AgentStore — instructions fields", () => { role: "executor", instructionsText: "Always use TypeScript strict mode.", }); + createdAgentIds.push(agent.id); expect(agent.instructionsText).toBe("Always use TypeScript strict mode."); expect(agent.instructionsPath).toBeUndefined(); @@ -35,6 +53,7 @@ describe("AgentStore — instructions fields", () => { role: "executor", instructionsPath: ".fusion/agents/custom.md", }); + createdAgentIds.push(agent.id); expect(agent.instructionsPath).toBe(".fusion/agents/custom.md"); expect(agent.instructionsText).toBeUndefined(); @@ -47,6 +66,7 @@ describe("AgentStore — instructions fields", () => { instructionsText: "Check for security issues.", instructionsPath: ".fusion/agents/reviewer.md", }); + createdAgentIds.push(agent.id); expect(agent.instructionsText).toBe("Check for security issues."); expect(agent.instructionsPath).toBe(".fusion/agents/reviewer.md"); @@ -57,6 +77,7 @@ describe("AgentStore — instructions fields", () => { name: "test-agent", role: "executor", }); + createdAgentIds.push(agent.id); expect(agent.instructionsText).toBeUndefined(); expect(agent.instructionsPath).toBeUndefined(); @@ -68,6 +89,7 @@ describe("AgentStore — instructions fields", () => { role: "executor", instructionsText: "Always write tests.", }); + createdAgentIds.push(created.id); const loaded = await store.getAgent(created.id); expect(loaded).not.toBeNull(); @@ -80,6 +102,7 @@ describe("AgentStore — instructions fields", () => { role: "executor", instructionsPath: ".fusion/agents/instructions.md", }); + createdAgentIds.push(created.id); const loaded = await store.getAgent(created.id); expect(loaded).not.toBeNull(); @@ -91,6 +114,7 @@ describe("AgentStore — instructions fields", () => { name: "test-agent", role: "executor", }); + createdAgentIds.push(agent.id); const updated = await store.updateAgent(agent.id, { instructionsText: "Use functional programming patterns.", @@ -104,6 +128,7 @@ describe("AgentStore — instructions fields", () => { name: "test-agent", role: "executor", }); + createdAgentIds.push(agent.id); const updated = await store.updateAgent(agent.id, { instructionsPath: ".fusion/agents/new-instructions.md", @@ -118,6 +143,7 @@ describe("AgentStore — instructions fields", () => { role: "executor", instructionsText: "Some instructions", }); + createdAgentIds.push(agent.id); const updated = await store.updateAgent(agent.id, { instructionsText: "", @@ -133,6 +159,7 @@ describe("AgentStore — instructions fields", () => { role: "executor", instructionsPath: ".fusion/agents/old.md", }); + createdAgentIds.push(agent.id); const updated = await store.updateAgent(agent.id, { instructionsPath: "", @@ -148,6 +175,7 @@ describe("AgentStore — instructions fields", () => { instructionsText: "Old text", instructionsPath: "old.md", }); + createdAgentIds.push(agent.id); const updated = await store.updateAgent(agent.id, { instructionsText: "New text", @@ -170,6 +198,7 @@ describe("AgentStore — instructions fields", () => { title: "My Executor", instructionsText: "Initial", }); + createdAgentIds.push(agent.id); const updated = await store.updateAgent(agent.id, { instructionsText: "Updated", @@ -188,6 +217,7 @@ describe("AgentStore — instructions fields", () => { instructionsText: "Cached instructions", instructionsPath: ".fusion/cached.md", }); + createdAgentIds.push(agent.id); const cached = store.getCachedAgent(agent.id); expect(cached).not.toBeNull(); diff --git a/packages/core/src/__tests__/central-integration.test.ts b/packages/core/src/__tests__/central-integration.test.ts index 63f878f63..5ae1d3d5b 100644 --- a/packages/core/src/__tests__/central-integration.test.ts +++ b/packages/core/src/__tests__/central-integration.test.ts @@ -30,9 +30,30 @@ describe("CentralCore Integration", () => { }); afterAll(async () => { - // Cleanup - await central.close(); - rmSync(tempDir, { recursive: true, force: true }); + // Teardown order: entity cleanup first, then infrastructure, then filesystem + // Unregister all tracked projects first (not just a subset from assertions) + for (const project of projects) { + try { + await central.unregisterProject(project.id); + } catch { + // Ignore cleanup errors for already-removed entities + } + } + projects.length = 0; + + // Close CentralCore before filesystem cleanup + try { + await central.close(); + } catch { + // Ignore close errors + } + + // Filesystem cleanup last + try { + rmSync(tempDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } }); it("should register multiple projects", async () => {