Files
fusion/packages/engine/src/__tests__/agent-tools-delegation.test.ts
2026-05-18 19:44:33 -07:00

412 lines
16 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Agent, AgentStore, TaskStore, Task } from "@fusion/core";
import { createListAgentsTool, createDelegateTaskTool } from "../agent-tools.js";
function createMockAgentStore(overrides: Partial<AgentStore> = {}): AgentStore {
return {
listAgents: vi.fn().mockResolvedValue([]),
getAgent: vi.fn().mockResolvedValue(null),
...overrides,
} as unknown as AgentStore;
}
function createMockTaskStore(overrides: Partial<TaskStore> = {}): TaskStore {
return {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({
id: "FN-001",
description: "",
dependencies: [],
column: "triage" as const,
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
}),
...overrides,
} as unknown as TaskStore;
}
function createAgent(overrides: Partial<Agent> = {}): Agent {
const now = new Date().toISOString();
return {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
createdAt: now,
updatedAt: now,
metadata: {},
...overrides,
};
}
describe("createListAgentsTool", () => {
let agentStore: AgentStore;
beforeEach(() => {
agentStore = createMockAgentStore();
});
it("returns formatted list of agents with their details", async () => {
const agents = [
createAgent({ id: "agent-001", name: "Alice", role: "executor", state: "idle", taskId: undefined }),
createAgent({ id: "agent-002", name: "Bob", role: "reviewer", state: "running", taskId: "FN-100" }),
];
vi.mocked(agentStore.listAgents).mockResolvedValue(agents);
const tool = createListAgentsTool(agentStore);
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
expect(result.content[0]).toHaveProperty("text");
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("Available agents:");
expect(text).toContain("ID: agent-001");
expect(text).toContain("Name: Alice");
expect(text).toContain("Role: executor");
expect(text).toContain("State: idle");
expect(text).toContain("ID: agent-002");
expect(text).toContain("Name: Bob");
expect(text).toContain("Role: reviewer");
expect(text).toContain("State: running");
expect(text).toContain("Current Task: FN-100");
});
it("includes soul truncated to 200 chars when present", async () => {
const longSoul = "A".repeat(300);
const agent = createAgent({ id: "agent-001", soul: longSoul });
vi.mocked(agentStore.listAgents).mockResolvedValue([agent]);
const tool = createListAgentsTool(agentStore);
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("Soul: " + "A".repeat(200));
expect(text).not.toContain("Soul: " + "A".repeat(201));
});
it("includes title when present", async () => {
const agent = createAgent({ id: "agent-001", title: "Senior Engineer" });
vi.mocked(agentStore.listAgents).mockResolvedValue([agent]);
const tool = createListAgentsTool(agentStore);
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("Title: Senior Engineer");
});
it("includes instructionsText summary when present", async () => {
const agent = createAgent({ id: "agent-001", instructionsText: "Be thorough and check edge cases." });
vi.mocked(agentStore.listAgents).mockResolvedValue([agent]);
const tool = createListAgentsTool(agentStore);
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("Custom Instructions: Be thorough and check edge cases.");
});
it("includes instructionsText truncated to 100 chars with ellipsis", async () => {
const longInstructions = "X".repeat(150);
const agent = createAgent({ id: "agent-001", instructionsText: longInstructions });
vi.mocked(agentStore.listAgents).mockResolvedValue([agent]);
const tool = createListAgentsTool(agentStore);
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("Custom Instructions: " + "X".repeat(100) + "…");
expect(text).not.toContain("Custom Instructions: " + "X".repeat(101));
});
it("filters by role when provided", async () => {
const tool = createListAgentsTool(agentStore);
await tool.execute("session-1", { role: "executor" }, undefined as any, undefined as any, undefined as any);
expect(agentStore.listAgents).toHaveBeenCalledWith({ role: "executor" });
});
it("filters by state when provided", async () => {
const tool = createListAgentsTool(agentStore);
await tool.execute("session-1", { state: "idle" }, undefined as any, undefined as any, undefined as any);
expect(agentStore.listAgents).toHaveBeenCalledWith({ state: "idle" });
});
it("passes includeEphemeral when provided", async () => {
const tool = createListAgentsTool(agentStore);
await tool.execute("session-1", { includeEphemeral: true }, undefined as any, undefined as any, undefined as any);
expect(agentStore.listAgents).toHaveBeenCalledWith({ includeEphemeral: true });
});
it("returns no-agents message when list is empty", async () => {
vi.mocked(agentStore.listAgents).mockResolvedValue([]);
const tool = createListAgentsTool(agentStore);
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("No agents found matching the specified filters.");
});
});
describe("createDelegateTaskTool", () => {
let agentStore: AgentStore;
let taskStore: TaskStore;
beforeEach(() => {
agentStore = createMockAgentStore();
taskStore = createMockTaskStore();
});
it("creates task with correct assignedAgentId, column todo, and description", async () => {
const agent = createAgent({ id: "agent-001", name: "Bob" });
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);
vi.mocked(taskStore.createTask).mockResolvedValue({
id: "FN-050",
description: "Write tests",
dependencies: [],
column: "todo" as const,
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const tool = createDelegateTaskTool(agentStore, taskStore);
const result = await tool.execute("session-1", {
agent_id: "agent-001",
description: "Write tests",
}, undefined as any, undefined as any, undefined as any);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
description: "Write tests",
dependencies: undefined,
column: "todo",
assignedAgentId: "agent-001",
source: expect.objectContaining({ sourceType: "api" }),
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("Delegated to Bob (agent-001)");
expect(text).toContain("Created FN-050");
expect(text).toContain("picked up by Bob on their next heartbeat cycle");
});
it("returns success message with task ID and agent name", async () => {
const agent = createAgent({ id: "agent-001", name: "Bob" });
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);
vi.mocked(taskStore.createTask).mockResolvedValue({
id: "FN-051",
description: "Write tests",
dependencies: [],
column: "todo" as const,
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const tool = createDelegateTaskTool(agentStore, taskStore);
const result = await tool.execute("session-1", {
agent_id: "agent-001",
description: "Write tests",
}, undefined as any, undefined as any, undefined as any);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("FN-051");
expect(text).toContain("Bob");
expect(result.details).toEqual({ taskId: "FN-051", agentId: "agent-001", agentName: "Bob" });
});
it("returns error when target agent not found", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(null);
const tool = createDelegateTaskTool(agentStore, taskStore);
const result = await tool.execute("session-1", {
agent_id: "nonexistent-agent",
description: "Do something",
}, undefined as any, undefined as any, undefined as any);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("ERROR: Agent nonexistent-agent not found");
expect(taskStore.createTask).not.toHaveBeenCalled();
});
it("returns error when target agent is ephemeral", async () => {
const ephemeralAgent = createAgent({
id: "executor-FN-100",
metadata: { agentKind: "task-worker" },
});
vi.mocked(agentStore.getAgent).mockResolvedValue(ephemeralAgent);
const tool = createDelegateTaskTool(agentStore, taskStore);
const result = await tool.execute("session-1", {
agent_id: "executor-FN-100",
description: "Do something",
}, undefined as any, undefined as any, undefined as any);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("ERROR: Cannot delegate to ephemeral/runtime agent executor-FN-100");
expect(taskStore.createTask).not.toHaveBeenCalled();
});
it("returns explicit collision error when delegated createTask hits existing id", async () => {
const agent = createAgent({ id: "agent-001", name: "Bob" });
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);
vi.mocked(taskStore.createTask).mockRejectedValue(new Error("Task ID already exists: FN-050"));
const tool = createDelegateTaskTool(agentStore, taskStore);
const result = await tool.execute("session-1", {
agent_id: "agent-001",
description: "Write tests",
}, undefined as any, undefined as any, undefined as any);
expect((result as { isError?: boolean }).isError).toBe(true);
expect((result.content[0] as { text: string }).text).toBe("ERROR: Task ID already exists: FN-050");
expect(result.details).toEqual({});
});
it("allows durable engineer target without override", async () => {
const engineer = createAgent({ id: "agent-009", name: "Eli", role: "engineer" });
vi.mocked(agentStore.getAgent).mockResolvedValue(engineer);
vi.mocked(taskStore.createTask).mockResolvedValue({
id: "FN-053",
description: "Do something",
dependencies: [],
column: "todo" as const,
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const tool = createDelegateTaskTool(agentStore, taskStore);
await tool.execute("session-1", {
agent_id: "agent-009",
description: "Do something",
}, undefined as any, undefined as any, undefined as any);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
assignedAgentId: "agent-009",
source: expect.objectContaining({ sourceType: "api" }),
}), expect.anything());
});
it("rejects reviewer target without override", async () => {
const reviewer = createAgent({ id: "agent-002", name: "Rita", role: "reviewer" });
vi.mocked(agentStore.getAgent).mockResolvedValue(reviewer);
const tool = createDelegateTaskTool(agentStore, taskStore);
const result = await tool.execute("session-1", {
agent_id: "agent-002",
description: "Do something",
}, undefined as any, undefined as any, undefined as any);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("ERROR: Agent agent-002 has role \"reviewer\"");
expect(text).toContain("Pass override=true to bypass");
expect(taskStore.createTask).not.toHaveBeenCalled();
});
it("allows non-executor target with override", async () => {
const reviewer = createAgent({ id: "agent-002", name: "Rita", role: "reviewer" });
vi.mocked(agentStore.getAgent).mockResolvedValue(reviewer);
vi.mocked(taskStore.createTask).mockResolvedValue({
id: "FN-054",
description: "Do something",
dependencies: [],
column: "todo" as const,
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const tool = createDelegateTaskTool(agentStore, taskStore);
await tool.execute("session-1", {
agent_id: "agent-002",
description: "Do something",
override: true,
}, undefined as any, undefined as any, undefined as any);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
source: expect.objectContaining({
sourceType: "api",
sourceMetadata: expect.objectContaining({ executorRoleOverride: true }),
}),
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
});
it("passes dependencies through to task creation", async () => {
const agent = createAgent({ id: "agent-001", name: "Bob" });
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);
vi.mocked(taskStore.createTask).mockResolvedValue({
id: "FN-052",
description: "Integration test",
dependencies: ["FN-010"],
column: "todo" as const,
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const tool = createDelegateTaskTool(agentStore, taskStore);
const result = await tool.execute("session-1", {
agent_id: "agent-001",
description: "Integration test",
dependencies: ["FN-010"],
}, undefined as any, undefined as any, undefined as any);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
description: "Integration test",
dependencies: ["FN-010"],
column: "todo",
assignedAgentId: "agent-001",
source: expect.objectContaining({ sourceType: "api" }),
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("depends on: FN-010");
});
it("creates task without dependencies when none specified", async () => {
const agent = createAgent({ id: "agent-001", name: "Bob" });
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);
vi.mocked(taskStore.createTask).mockResolvedValue({
id: "FN-053",
description: "Simple task",
dependencies: [],
column: "todo" as const,
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const tool = createDelegateTaskTool(agentStore, taskStore);
const result = await tool.execute("session-1", {
agent_id: "agent-001",
description: "Simple task",
}, undefined as any, undefined as any, undefined as any);
expect(taskStore.createTask).toHaveBeenCalledWith(
expect.objectContaining({ dependencies: undefined }),
expect.objectContaining({ settings: { autoSummarizeTitles: false } }),
);
const text = (result.content[0] as { text: string }).text;
expect(text).not.toContain("depends on:");
});
});