The vitest child-process guard wrapped exec/execFile without preserving the
`[util.promisify.custom]` symbol, so awaited `execAsync` resolved to a raw
stdout string instead of `{stdout, stderr}`. That single regression cascaded
through ~60 "failing" tests across cli, core, engine, and dashboard whose
production code was actually correct. Also relax the AI-CLI blocklist for
cheap introspection (--version/--help/which …), give SIGTERM'd subprocesses a
brief grace period before being flagged as "left running", fix a few real
test-side bugs uncovered along the way (executor mock step transitions, iOS
last-resort keyboard path, mission SSE replay tests racing with the real AI
agent), and convert dashboard route tests' dynamic `await import("../server.js")`
to static imports so first-test timings drop from 2–5s to <200ms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
199 lines
5.8 KiB
TypeScript
199 lines
5.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { EventEmitter } from "node:events";
|
|
import { request } from "../test-request.js";
|
|
import { createServer } from "../server.js";
|
|
|
|
const mockInit = vi.fn().mockResolvedValue(undefined);
|
|
const mockGetAgent = vi.fn();
|
|
const mockGetChainOfCommand = vi.fn();
|
|
const mockGetOrgTree = vi.fn();
|
|
const mockResolveAgent = vi.fn();
|
|
const mockListAgents = vi.fn().mockResolvedValue([]);
|
|
const mockChatStoreInit = vi.fn().mockResolvedValue(undefined);
|
|
|
|
vi.mock("@fusion/core", () => {
|
|
return {
|
|
AgentStore: class MockAgentStore {
|
|
init = mockInit;
|
|
getAgent = mockGetAgent;
|
|
getChainOfCommand = mockGetChainOfCommand;
|
|
getOrgTree = mockGetOrgTree;
|
|
resolveAgent = mockResolveAgent;
|
|
listAgents = mockListAgents;
|
|
},
|
|
ChatStore: class MockChatStore {
|
|
init = mockChatStoreInit;
|
|
},
|
|
};
|
|
});
|
|
|
|
class MockStore extends EventEmitter {
|
|
getRootDir(): string {
|
|
return "/tmp/fn-1165-test";
|
|
}
|
|
|
|
getFusionDir(): string {
|
|
return "/tmp/fn-1165-test/.fusion";
|
|
}
|
|
|
|
getDatabase() {
|
|
return {
|
|
exec: vi.fn(),
|
|
prepare: vi.fn().mockReturnValue({
|
|
run: vi.fn().mockReturnValue({ changes: 0 }),
|
|
get: vi.fn(),
|
|
all: vi.fn().mockReturnValue([]),
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
function createMockAgent(id: string, name: string, reportsTo?: string) {
|
|
return {
|
|
id,
|
|
name,
|
|
role: "executor",
|
|
state: "idle",
|
|
metadata: {},
|
|
reportsTo,
|
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
describe("Agent org chart routes", () => {
|
|
let store: MockStore;
|
|
let app: ReturnType<typeof createServer>;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
mockInit.mockResolvedValue(undefined);
|
|
mockListAgents.mockResolvedValue([]);
|
|
|
|
store = new MockStore();
|
|
app = createServer(store as any);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("GET /api/agents/:id/chain-of-command", () => {
|
|
it("returns chain of command for a valid agent", async () => {
|
|
const self = createMockAgent("agent-001", "Builder Bot", "agent-010");
|
|
const manager = createMockAgent("agent-010", "Manager Bot");
|
|
mockGetAgent.mockResolvedValue(self);
|
|
mockGetChainOfCommand.mockResolvedValue([self, manager]);
|
|
|
|
const response = await request(app, "GET", "/api/agents/agent-001/chain-of-command");
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual([self, manager]);
|
|
expect(mockGetChainOfCommand).toHaveBeenCalledWith("agent-001");
|
|
});
|
|
|
|
it("returns 404 when agent is not found", async () => {
|
|
mockGetAgent.mockResolvedValue(null);
|
|
|
|
const response = await request(app, "GET", "/api/agents/missing-agent/chain-of-command");
|
|
|
|
expect(response.status).toBe(404);
|
|
expect((response.body as any).error).toBe("Agent not found");
|
|
expect(mockGetChainOfCommand).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("GET /api/agents/org-tree", () => {
|
|
it("returns empty array for empty store", async () => {
|
|
mockGetOrgTree.mockResolvedValue([]);
|
|
mockListAgents.mockResolvedValue([]);
|
|
|
|
const response = await request(app, "GET", "/api/agents/org-tree");
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual([]);
|
|
});
|
|
|
|
it("returns populated org tree", async () => {
|
|
const ceo = createMockAgent("agent-ceo", "CEO Bot");
|
|
const lead = createMockAgent("agent-lead", "Lead Bot", "agent-ceo");
|
|
mockListAgents.mockResolvedValue([ceo, lead]);
|
|
mockGetOrgTree.mockResolvedValue([
|
|
{
|
|
agent: ceo,
|
|
children: [
|
|
{
|
|
agent: lead,
|
|
children: [],
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
const response = await request(app, "GET", "/api/agents/org-tree");
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual([
|
|
{
|
|
agent: ceo,
|
|
children: [
|
|
{
|
|
agent: lead,
|
|
children: [],
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("GET /api/agents/resolve/:shortname", () => {
|
|
it("resolves by shortname", async () => {
|
|
const agent = createMockAgent("agent-001", "Build Agent");
|
|
mockListAgents.mockResolvedValue([agent]);
|
|
mockResolveAgent.mockResolvedValue(agent);
|
|
|
|
const response = await request(app, "GET", "/api/agents/resolve/build-agent");
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({ agent });
|
|
expect(mockResolveAgent).toHaveBeenCalledWith("build-agent");
|
|
});
|
|
|
|
it("resolves by ID", async () => {
|
|
const agent = createMockAgent("agent-001", "Build Agent");
|
|
mockListAgents.mockResolvedValue([agent]);
|
|
mockResolveAgent.mockResolvedValue(agent);
|
|
|
|
const response = await request(app, "GET", "/api/agents/resolve/agent-001");
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({ agent });
|
|
expect(mockResolveAgent).toHaveBeenCalledWith("agent-001");
|
|
});
|
|
|
|
it("returns 404 when not found", async () => {
|
|
mockListAgents.mockResolvedValue([]);
|
|
mockResolveAgent.mockResolvedValue(null);
|
|
|
|
const response = await request(app, "GET", "/api/agents/resolve/missing-agent");
|
|
|
|
expect(response.status).toBe(404);
|
|
expect((response.body as any).error).toBe("Agent not found");
|
|
});
|
|
|
|
it("returns 404 when ambiguous", async () => {
|
|
mockListAgents.mockResolvedValue([
|
|
createMockAgent("agent-001", "Build Agent"),
|
|
createMockAgent("agent-002", "Build Agent"),
|
|
]);
|
|
mockResolveAgent.mockResolvedValue(null);
|
|
|
|
const response = await request(app, "GET", "/api/agents/resolve/build-agent");
|
|
|
|
expect(response.status).toBe(404);
|
|
expect((response.body as any).error).toBe("Agent not found");
|
|
});
|
|
});
|
|
});
|