Files
fusion/packages/engine/src/__tests__/task-agent-sync.test.ts
Fusion b5b7ca9b09 feat(FN-4296): complete Step 5 — finalize sync wiring and verification fixes
Fusion-Task-Id: FN-4296
Fusion-Task-Lineage: 9d061861-547a-4905-8fdd-4b66dd7bff97
2026-05-14 05:35:35 -07:00

132 lines
5.1 KiB
TypeScript

import { EventEmitter } from "node:events";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { AgentStore, type AgentCreateInput, type Task } from "@fusion/core";
import { describe, expect, it, vi } from "vitest";
import { attachAgentLinkSync } from "../task-agent-sync.js";
class EventedStore extends EventEmitter {
on(event: "task:moved", listener: (data: { task: Task; from: string; to: string }) => void): this {
return super.on(event, listener);
}
off(event: "task:moved", listener: (data: { task: Task; from: string; to: string }) => void): this {
return super.off(event, listener);
}
}
const createInput: AgentCreateInput = { name: "durable-agent", role: "executor" };
describe("FN-4296: task agent sync", () => {
const runCase = async (to: string, hasActiveAgentExecution = false) => {
const store = new EventedStore();
const agentStore = {
listAgents: vi.fn(async () => [{ id: "agent-1", taskId: "FN-1" }]),
syncExecutionTaskLink: vi.fn(async () => undefined),
assignTask: vi.fn(async () => undefined),
} as any;
const detach = attachAgentLinkSync({
store: store as any,
agentStore,
hasActiveAgentExecution: () => hasActiveAgentExecution,
logger: { log: vi.fn(), warn: vi.fn() },
});
store.emit("task:moved", { task: { id: "FN-1" }, from: "in-progress", to });
await Promise.resolve();
await Promise.resolve();
return { detach, agentStore };
};
it("FN-4296: task:moved → done clears linked durable agent's taskId", async () => {
const { agentStore } = await runCase("done");
expect(agentStore.syncExecutionTaskLink).toHaveBeenCalledWith("agent-1", undefined);
});
it("FN-4296: task:moved → archived clears link", async () => {
const { agentStore } = await runCase("archived");
expect(agentStore.syncExecutionTaskLink).toHaveBeenCalledWith("agent-1", undefined);
});
it("FN-4296: task:moved → todo clears link when no in-flight execution", async () => {
const { agentStore } = await runCase("todo", false);
expect(agentStore.syncExecutionTaskLink).toHaveBeenCalledWith("agent-1", undefined);
});
it("FN-4296: task:moved → todo does NOT clear link when hasActiveAgentExecution=true", async () => {
const { agentStore } = await runCase("todo", true);
expect(agentStore.syncExecutionTaskLink).not.toHaveBeenCalled();
});
it("FN-4296: task:moved → triage clears link", async () => {
const { agentStore } = await runCase("triage", false);
expect(agentStore.syncExecutionTaskLink).toHaveBeenCalledWith("agent-1", undefined);
});
it("FN-4296: task:moved → in-review does NOT clear link", async () => {
const { agentStore } = await runCase("in-review", false);
expect(agentStore.syncExecutionTaskLink).not.toHaveBeenCalled();
});
it("FN-4296: task:moved → in-progress does NOT clear link", async () => {
const { agentStore } = await runCase("in-progress", false);
expect(agentStore.syncExecutionTaskLink).not.toHaveBeenCalled();
});
it("FN-4296: returned detach function unsubscribes the listener", async () => {
const store = new EventedStore();
const agentStore = {
listAgents: vi.fn(async () => [{ id: "agent-1", taskId: "FN-1" }]),
syncExecutionTaskLink: vi.fn(async () => undefined),
assignTask: vi.fn(async () => undefined),
} as any;
const detach = attachAgentLinkSync({ store: store as any, agentStore, logger: { log: vi.fn(), warn: vi.fn() } });
detach();
store.emit("task:moved", { task: { id: "FN-1" }, from: "in-progress", to: "done" });
await Promise.resolve();
expect(agentStore.syncExecutionTaskLink).not.toHaveBeenCalled();
});
it("FN-4296: clear uses syncExecutionTaskLink not assignTask", async () => {
const { agentStore } = await runCase("done", false);
expect(agentStore.syncExecutionTaskLink).toHaveBeenCalled();
expect(agentStore.assignTask).not.toHaveBeenCalled();
});
it("FN-4296: integration-flavored clear persists on real AgentStore", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "fn-4296-agent-store-"));
try {
const store = new EventedStore();
const agentStore = new AgentStore({ rootDir, inMemoryDb: true });
const created = await agentStore.createAgent(createInput);
await agentStore.syncExecutionTaskLink(created.id, "FN-REAL");
const logger = { log: vi.fn(), warn: vi.fn() };
const detach = attachAgentLinkSync({
store: store as any,
agentStore,
logger,
});
store.emit("task:moved", { task: { id: "FN-REAL" } as Task, from: "in-progress", to: "done" });
let hydrated = await agentStore.getAgent(created.id);
for (let attempt = 0; attempt < 10 && hydrated?.taskId; attempt += 1) {
await new Promise((resolve) => setTimeout(resolve, 5));
hydrated = await agentStore.getAgent(created.id);
}
expect(logger.warn).not.toHaveBeenCalled();
expect(hydrated?.taskId).toBeUndefined();
detach();
await agentStore.close();
} finally {
rmSync(rootDir, { recursive: true, force: true });
}
});
});