refactor(FN-2162): rename kb-agent identifiers to fn-agent
- Rename core loader, dashboard server chat/planning routes, and frontend agent IDs/storage keys from kb-agent to fn-agent naming - Update dashboard hooks and components (agent list, chat view, quick chat) to use the new fn agent key prefixes consistently - Refresh engine, dashboard, core, and CLI tests/mocks to remove remaining kb-agent route and temp prefix references - Update storage/gap-analysis docs to reflect fn agent key names and add a @gsxdsm/fusion patch changeset for the rename
This commit is contained in:
@@ -39,7 +39,7 @@ vi.mock("./pi.js", () => ({
|
||||
// Import the mocked functions for test control
|
||||
import { createFnAgent } from "./pi.js";
|
||||
import { heartbeatLog } from "./logger.js";
|
||||
const mockedCreateKbAgent = vi.mocked(createFnAgent);
|
||||
const mockedCreateFnAgent = vi.mocked(createFnAgent);
|
||||
|
||||
// Mock store factory
|
||||
function createMockStore(overrides: Partial<AgentStore> = {}): AgentStore {
|
||||
@@ -1359,7 +1359,7 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(result.status).toBe("completed");
|
||||
expect(result.resultJson).toEqual({ reason: "no_assignment" });
|
||||
// Should NOT have created an agent session
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("completes with invalid_state when agent state is terminated", async () => {
|
||||
@@ -1371,7 +1371,7 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe("completed");
|
||||
expect(result.resultJson).toEqual({ reason: "invalid_state", state: "terminated" });
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "active");
|
||||
});
|
||||
|
||||
@@ -1408,7 +1408,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("agent WITH soul but no task creates session and completes successfully", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator agent who monitors project health" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
@@ -1417,7 +1417,7 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe("completed");
|
||||
// Should create a session
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
// Reason should indicate identity run
|
||||
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "no_assignment_identity_run" }));
|
||||
});
|
||||
@@ -1425,7 +1425,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("agent WITH instructionsText but no task creates session and completes successfully", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, instructionsText: "Monitor task board and create follow-up tasks" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
@@ -1433,14 +1433,14 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe("completed");
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "no_assignment_identity_run" }));
|
||||
});
|
||||
|
||||
it("agent WITH memory but no task creates session and completes successfully", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, memory: "Last week we shipped the new API. Watch for integration issues." });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
@@ -1448,7 +1448,7 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe("completed");
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "no_assignment_identity_run" }));
|
||||
});
|
||||
|
||||
@@ -1460,7 +1460,7 @@ describe("HeartbeatMonitor", () => {
|
||||
metadata: { agentKind: "task-worker" },
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
@@ -1469,7 +1469,7 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe("completed");
|
||||
// Ephemeral agents should NOT create a session
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
// Should still exit with no_assignment (not no_assignment_identity_run)
|
||||
expect(result.resultJson).toEqual({ reason: "no_assignment" });
|
||||
});
|
||||
@@ -1477,14 +1477,14 @@ describe("HeartbeatMonitor", () => {
|
||||
it("identity agent without task receives correct tools (task_create, list_agents, delegate_task, heartbeat_done)", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0]!;
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
|
||||
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
|
||||
|
||||
// Should have task_create, list_agents, delegate_task
|
||||
@@ -1506,14 +1506,14 @@ describe("HeartbeatMonitor", () => {
|
||||
it("no-task run receives HEARTBEAT_NO_TASK_SYSTEM_PROMPT as system prompt", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0]!;
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
|
||||
const systemPrompt = callArgs.systemPrompt;
|
||||
|
||||
expect(systemPrompt).toContain(HEARTBEAT_NO_TASK_SYSTEM_PROMPT);
|
||||
@@ -1533,14 +1533,14 @@ describe("HeartbeatMonitor", () => {
|
||||
it("identity agent without task receives no-task execution prompt mentioning 'no assigned task'", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0]!;
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
|
||||
const systemPrompt = callArgs.systemPrompt;
|
||||
expect(systemPrompt).toContain(HEARTBEAT_NO_TASK_SYSTEM_PROMPT);
|
||||
expect(systemPrompt).not.toContain("task_log");
|
||||
@@ -1571,14 +1571,14 @@ describe("HeartbeatMonitor", () => {
|
||||
it("task-scoped run receives HEARTBEAT_SYSTEM_PROMPT as system prompt", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0]!;
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
|
||||
const systemPrompt = callArgs.systemPrompt;
|
||||
|
||||
expect(systemPrompt).toContain(HEARTBEAT_SYSTEM_PROMPT);
|
||||
@@ -1590,14 +1590,14 @@ describe("HeartbeatMonitor", () => {
|
||||
it("identity agent without task gets soul in system prompt", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a CEO who prioritizes high-impact work" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0]!;
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
|
||||
// Soul should be in the system prompt
|
||||
expect(callArgs.systemPrompt).toContain("## Soul");
|
||||
expect(callArgs.systemPrompt).toContain("I am a CEO who prioritizes high-impact work");
|
||||
@@ -1612,7 +1612,7 @@ describe("HeartbeatMonitor", () => {
|
||||
memory: "",
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
@@ -1621,14 +1621,14 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe("completed");
|
||||
// Should NOT create a session for agents without identity
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
expect(result.resultJson).toEqual({ reason: "no_assignment" });
|
||||
});
|
||||
|
||||
it("identity agent without task includes messaging tools when messageStore is available", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const messageStore = {
|
||||
setMessageToAgentHook: vi.fn(),
|
||||
@@ -1645,8 +1645,8 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0]!;
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
|
||||
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
|
||||
|
||||
// Should have messaging tools when messageStore is available
|
||||
@@ -1657,14 +1657,14 @@ describe("HeartbeatMonitor", () => {
|
||||
it("identity agent without task does NOT include messaging tools when messageStore is unavailable", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0]!;
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
|
||||
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
|
||||
|
||||
// Should NOT have messaging tools when messageStore is not available
|
||||
@@ -1723,7 +1723,7 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(result.resultJson).toEqual({ reason: "blocked_duplicate", taskId: "FN-BLOCKED", blockedBy: "FN-DEP-1" });
|
||||
expect(mockTaskStore.addComment).not.toHaveBeenCalled();
|
||||
expect(store.setLastBlockedState).not.toHaveBeenCalled();
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("re-logs blocked state when new comments change context hash", async () => {
|
||||
@@ -1764,7 +1764,7 @@ describe("HeartbeatMonitor", () => {
|
||||
"agent-001",
|
||||
expect.objectContaining({ taskId: "FN-BLOCKED", blockedBy: "FN-DEP-1" }),
|
||||
);
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats changed blockedBy as a new blocked state", async () => {
|
||||
@@ -1809,7 +1809,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("clears blocked state when task is no longer blocked", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: "FN-READY" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
mockTaskStore = createMockTaskStore({
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
@@ -1864,7 +1864,7 @@ describe("HeartbeatMonitor", () => {
|
||||
// Create the heartbeat monitor (it does NOT receive the task-lane semaphore)
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
@@ -1878,7 +1878,7 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(result.status).toBe("completed");
|
||||
|
||||
// 2. Agent session was created (proves execution proceeded)
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
|
||||
// 3. Semaphore saturation is still held (proves heartbeat didn't consume task-lane slot)
|
||||
expect(taskLaneSemaphore.activeCount).toBe(1);
|
||||
@@ -1906,7 +1906,7 @@ describe("HeartbeatMonitor", () => {
|
||||
// Now execute heartbeat - it should complete without waiting
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
@@ -1933,7 +1933,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("includes unread messages in prompt when woken by wake-on-message", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const messages = [
|
||||
createMessage({
|
||||
@@ -1970,7 +1970,7 @@ describe("HeartbeatMonitor", () => {
|
||||
});
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
expect(messageStore.getInbox).toHaveBeenCalledWith("agent-001", "agent", { read: false, limit: 10 });
|
||||
|
||||
// Verify execution prompt (passed to promptWithFallback) included the messages
|
||||
@@ -1986,7 +1986,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("does not include message section when no unread messages", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const messageStore = {
|
||||
setMessageToAgentHook: vi.fn(),
|
||||
@@ -2017,7 +2017,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("marks messages as read after successful heartbeat execution", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const messages = [
|
||||
createMessage({
|
||||
@@ -2054,7 +2054,7 @@ describe("HeartbeatMonitor", () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockSession.prompt = vi.fn().mockRejectedValue(new Error("Execution failed"));
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const messages = [
|
||||
createMessage({
|
||||
@@ -2090,7 +2090,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("does not fetch messages when not wake-on-message trigger", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const messageStore = {
|
||||
setMessageToAgentHook: vi.fn(),
|
||||
@@ -2189,7 +2189,7 @@ describe("HeartbeatMonitor", () => {
|
||||
taskId: "FN-001",
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({
|
||||
store,
|
||||
@@ -2264,7 +2264,7 @@ describe("HeartbeatMonitor", () => {
|
||||
});
|
||||
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
@@ -2280,7 +2280,7 @@ describe("HeartbeatMonitor", () => {
|
||||
mockTaskStore = createMockTaskStore({ selectNextTaskForAgent });
|
||||
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
await monitor.executeHeartbeat({
|
||||
@@ -2299,7 +2299,7 @@ describe("HeartbeatMonitor", () => {
|
||||
mockTaskStore = createMockTaskStore({ selectNextTaskForAgent });
|
||||
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
@@ -2340,7 +2340,7 @@ describe("HeartbeatMonitor", () => {
|
||||
});
|
||||
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
@@ -2372,7 +2372,7 @@ describe("HeartbeatMonitor", () => {
|
||||
});
|
||||
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
@@ -2400,7 +2400,7 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(selectNextTaskForAgent).toHaveBeenCalledWith("agent-001");
|
||||
expect(checkoutTask).toHaveBeenCalledWith("FN-CHECKOUT", "agent-001", expect.objectContaining({ agentId: "agent-001" }));
|
||||
expect(result.resultJson).toEqual({ reason: "no_assignment" });
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2447,7 +2447,7 @@ describe("HeartbeatMonitor", () => {
|
||||
instructionsText: "Always log blockers with actionable next steps.",
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2455,8 +2455,8 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
expect(callArgs.cwd).toBe("/tmp/test");
|
||||
expect(callArgs.systemPrompt).toContain(HEARTBEAT_SYSTEM_PROMPT);
|
||||
expect(callArgs.systemPrompt).toContain("## Soul");
|
||||
@@ -2493,7 +2493,7 @@ describe("HeartbeatMonitor", () => {
|
||||
instructionsPath: undefined,
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2501,7 +2501,7 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
expect(callArgs.systemPrompt).toContain(HEARTBEAT_SYSTEM_PROMPT);
|
||||
expect(callArgs.systemPrompt).toContain("## Project Memory");
|
||||
});
|
||||
@@ -2512,7 +2512,7 @@ describe("HeartbeatMonitor", () => {
|
||||
getSettings: vi.fn().mockResolvedValue({ memoryEnabled: false }),
|
||||
} as Partial<TaskStore>);
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2520,7 +2520,7 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
|
||||
expect(callArgs.systemPrompt).not.toContain("## Project Memory");
|
||||
expect(toolNames).not.toContain("memory_search");
|
||||
@@ -2537,7 +2537,7 @@ describe("HeartbeatMonitor", () => {
|
||||
getSettings: vi.fn().mockResolvedValue({ memoryBackendType: "file" }),
|
||||
} as Partial<TaskStore>);
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2545,7 +2545,7 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
const memorySearch = callArgs.customTools!.find((tool: any) => tool.name === "memory_search") as any;
|
||||
expect(memorySearch).toBeDefined();
|
||||
const result = await memorySearch.execute("call-1", {
|
||||
@@ -2561,7 +2561,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("includes document tools in heartbeat session", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2569,7 +2569,7 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
const toolNames = callArgs.customTools!.map((t: any) => t.name);
|
||||
expect(toolNames).toContain("task_document_write");
|
||||
expect(toolNames).toContain("task_document_read");
|
||||
@@ -2578,7 +2578,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("heartbeat_done is the terminal tool (last in array)", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2586,7 +2586,7 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
const toolNames = callArgs.customTools!.map((t: any) => t.name);
|
||||
// heartbeat_done should be last for stable terminal signaling
|
||||
expect(toolNames[toolNames.length - 1]).toBe("heartbeat_done");
|
||||
@@ -2595,7 +2595,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("calls promptWithFallback with task context", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2615,7 +2615,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("includes triggering comment context in execution prompt when comment IDs are provided", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2653,7 +2653,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("keeps standard prompt when no triggering comments are provided", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2668,7 +2668,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("completes run with status completed on successful execution", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2687,7 +2687,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("uses explicit taskId override instead of agent.taskId", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: "FN-DEFAULT" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2717,7 +2717,7 @@ describe("HeartbeatMonitor", () => {
|
||||
// Should have fetched the override task
|
||||
expect(mockTaskStore.getTask).toHaveBeenCalledWith("FN-OVERRIDE");
|
||||
// task_log tool should use the override task ID
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
const taskLogTool = callArgs.customTools![1]!;
|
||||
expect(taskLogTool.name).toBe("task_log");
|
||||
});
|
||||
@@ -2727,7 +2727,7 @@ describe("HeartbeatMonitor", () => {
|
||||
runtimeConfig: { modelProvider: "openai", modelId: "gpt-4o" },
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2735,8 +2735,8 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
expect(callArgs.defaultProvider).toBe("openai");
|
||||
expect(callArgs.defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
@@ -2744,7 +2744,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("passes undefined model when runtimeConfig has no model", async () => {
|
||||
const store = createStoreWithAgentForExec({ runtimeConfig: {} });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2752,7 +2752,7 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
|
||||
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
expect(callArgs.defaultProvider).toBeUndefined();
|
||||
expect(callArgs.defaultModelId).toBeUndefined();
|
||||
});
|
||||
@@ -2760,7 +2760,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("persists contextSnapshot on run records", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -2798,7 +2798,7 @@ describe("HeartbeatMonitor", () => {
|
||||
let onToolStart: ((name: string, args?: Record<string, unknown>) => void) | undefined;
|
||||
let onToolEnd: ((name: string, isError: boolean, result?: unknown) => void) | undefined;
|
||||
|
||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
onText = opts.onText;
|
||||
onToolStart = opts.onToolStart;
|
||||
onToolEnd = opts.onToolEnd;
|
||||
@@ -2827,7 +2827,7 @@ describe("HeartbeatMonitor", () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
let capturedDoneTool: any;
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
// heartbeat_done is last in the customTools array (index 4)
|
||||
capturedDoneTool = opts.customTools[opts.customTools.length - 1];
|
||||
return { session: mockSession as any };
|
||||
@@ -2853,7 +2853,7 @@ describe("HeartbeatMonitor", () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
let capturedDoneTool: any;
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedDoneTool = opts.customTools[opts.customTools.length - 1];
|
||||
return { session: mockSession as any };
|
||||
});
|
||||
@@ -2876,7 +2876,7 @@ describe("HeartbeatMonitor", () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
let capturedCreateTool: any;
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedCreateTool = opts.customTools[0]; // task_create
|
||||
return { session: mockSession as any };
|
||||
});
|
||||
@@ -2900,7 +2900,7 @@ describe("HeartbeatMonitor", () => {
|
||||
describe("error handling", () => {
|
||||
it("completes run as failed when createFnAgent throws", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
mockedCreateKbAgent.mockRejectedValue(new Error("Model unavailable"));
|
||||
mockedCreateFnAgent.mockRejectedValue(new Error("Model unavailable"));
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
@@ -2916,7 +2916,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("completes run as failed when promptWithFallback throws", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
mockSession.prompt = vi.fn().mockRejectedValue(new Error("Prompt failed"));
|
||||
@@ -2939,7 +2939,7 @@ describe("HeartbeatMonitor", () => {
|
||||
const mockSession = createMockAgentSession();
|
||||
const flushSpy = vi.spyOn(AgentLogger.prototype, "flush").mockResolvedValue(undefined);
|
||||
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
mockSession.prompt = vi.fn().mockRejectedValue(new Error("Prompt failed"));
|
||||
@@ -2953,7 +2953,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("flushes AgentLogger when session creation fails", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const flushSpy = vi.spyOn(AgentLogger.prototype, "flush").mockResolvedValue(undefined);
|
||||
mockedCreateKbAgent.mockRejectedValue(new Error("Model unavailable"));
|
||||
mockedCreateFnAgent.mockRejectedValue(new Error("Model unavailable"));
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
@@ -2974,7 +2974,7 @@ describe("HeartbeatMonitor", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -3026,7 +3026,7 @@ describe("HeartbeatMonitor", () => {
|
||||
const mockSession = createMockAgentSession();
|
||||
let onTextCallback: ((delta: string) => void) | undefined;
|
||||
|
||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
onTextCallback = opts.onText;
|
||||
return { session: mockSession as any };
|
||||
});
|
||||
@@ -3057,7 +3057,7 @@ describe("HeartbeatMonitor", () => {
|
||||
const mockSession = createMockAgentSession();
|
||||
let onTextCallback: ((delta: string) => void) | undefined;
|
||||
|
||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
onTextCallback = opts.onText;
|
||||
return { session: mockSession as any };
|
||||
});
|
||||
@@ -3084,7 +3084,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("disposes session and untracks agent even on error", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
mockSession.prompt = vi.fn().mockRejectedValue(new Error("Crash"));
|
||||
@@ -3102,7 +3102,7 @@ describe("HeartbeatMonitor", () => {
|
||||
it("disposes session and untracks agent on success", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
@@ -3133,7 +3133,7 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(result.resultJson).toMatchObject({ reason: "budget_exhausted", budgetStatus });
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "active");
|
||||
});
|
||||
|
||||
@@ -3147,7 +3147,7 @@ describe("HeartbeatMonitor", () => {
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
|
||||
expect(result.resultJson).toMatchObject({ reason: "budget_exhausted" });
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips heartbeat when agent is over budget (assignment)", async () => {
|
||||
@@ -3160,7 +3160,7 @@ describe("HeartbeatMonitor", () => {
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
|
||||
|
||||
expect(result.resultJson).toMatchObject({ reason: "budget_exhausted" });
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips timer heartbeat when agent is over threshold but not over budget", async () => {
|
||||
@@ -3179,13 +3179,13 @@ describe("HeartbeatMonitor", () => {
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(result.resultJson).toMatchObject({ reason: "budget_threshold_exceeded", budgetStatus });
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows on_demand heartbeat when agent is over threshold", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||
createBudgetStatus({ isOverThreshold: true, usagePercent: 85, budgetLimit: 1000, thresholdPercent: 80 })
|
||||
);
|
||||
@@ -3194,13 +3194,13 @@ describe("HeartbeatMonitor", () => {
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("allows assignment heartbeat when agent is over threshold", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||
createBudgetStatus({ isOverThreshold: true, usagePercent: 85, budgetLimit: 1000, thresholdPercent: 80 })
|
||||
);
|
||||
@@ -3209,13 +3209,13 @@ describe("HeartbeatMonitor", () => {
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("proceeds normally when agent is below threshold", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||
createBudgetStatus({ isOverBudget: false, isOverThreshold: false, usagePercent: 30, budgetLimit: 1000, thresholdPercent: 80 })
|
||||
);
|
||||
@@ -3224,20 +3224,20 @@ describe("HeartbeatMonitor", () => {
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("proceeds normally when getBudgetStatus throws", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: mockSession as any });
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("budget unavailable"));
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4695,7 +4695,7 @@ describe("executeHeartbeat — skill selection resolver contract (FN-1510/FN-151
|
||||
// skills in metadata, the createFnAgent is called and the result includes skill info.
|
||||
|
||||
it("createFnAgent is called with agent session for heartbeat with skills", async () => {
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: createMockAgentSession(),
|
||||
} as any);
|
||||
|
||||
@@ -4707,12 +4707,12 @@ describe("executeHeartbeat — skill selection resolver contract (FN-1510/FN-151
|
||||
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
expect(result.status).toBe("completed");
|
||||
});
|
||||
|
||||
it("createFnAgent is called with correct cwd for skill resolution", async () => {
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: createMockAgentSession(),
|
||||
} as any);
|
||||
|
||||
@@ -4724,14 +4724,14 @@ describe("executeHeartbeat — skill selection resolver contract (FN-1510/FN-151
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateKbAgent.mock.calls[0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateFnAgent.mock.calls[0];
|
||||
const opts = firstCall[0];
|
||||
expect(opts.cwd).toBe("/project/root");
|
||||
});
|
||||
|
||||
it("heartbeat completes successfully when agent has no skills", async () => {
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: createMockAgentSession(),
|
||||
} as any);
|
||||
|
||||
@@ -4746,7 +4746,7 @@ describe("executeHeartbeat — skill selection resolver contract (FN-1510/FN-151
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe("completed");
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4881,7 +4881,7 @@ describe("executeHeartbeat — skill selection non-fatal (FN-1510/FN-1511)", ()
|
||||
// regardless of skill selection outcome
|
||||
|
||||
it("heartbeat completes when agent has empty metadata", async () => {
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: createMockAgentSession(),
|
||||
} as any);
|
||||
|
||||
@@ -4898,7 +4898,7 @@ describe("executeHeartbeat — skill selection non-fatal (FN-1510/FN-1511)", ()
|
||||
});
|
||||
|
||||
it("heartbeat completes when agent has various skill configurations", async () => {
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: createMockAgentSession(),
|
||||
} as any);
|
||||
|
||||
@@ -4922,7 +4922,7 @@ describe("executeHeartbeat — skill selection non-fatal (FN-1510/FN-1511)", ()
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import { createFnAgent, promptWithFallback } from "./pi.js";
|
||||
import { AgentReflectionService } from "./agent-reflection.js";
|
||||
import { createReflectOnPerformanceTool, reflectOnPerformanceParams } from "./agent-tools.js";
|
||||
|
||||
const mockedCreateKbAgent = vi.mocked(createFnAgent);
|
||||
const mockedCreateFnAgent = vi.mocked(createFnAgent);
|
||||
const mockedPromptWithFallback = vi.mocked(promptWithFallback);
|
||||
|
||||
function makeAgent(overrides: Partial<Agent> = {}): Agent {
|
||||
@@ -349,7 +349,7 @@ describe("AgentReflectionService", () => {
|
||||
const { agentStore, taskStore, reflectionStore } = createMockDeps();
|
||||
const session = createMockSession();
|
||||
|
||||
mockedCreateKbAgent.mockImplementation(async (options: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (options: any) => {
|
||||
options.onText?.(JSON.stringify({
|
||||
insights: ["Strong execution on scoped changes"],
|
||||
suggestedImprovements: ["Run tests earlier in the cycle"],
|
||||
@@ -379,7 +379,7 @@ describe("AgentReflectionService", () => {
|
||||
triggerDetail: "manual check",
|
||||
taskId: "FN-001",
|
||||
}));
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
tools: "readonly",
|
||||
}));
|
||||
});
|
||||
@@ -393,13 +393,13 @@ describe("AgentReflectionService", () => {
|
||||
const reflection = await service.generateReflection("agent-1", "manual");
|
||||
|
||||
expect(reflection).toBeNull();
|
||||
expect(mockedCreateKbAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
expect(reflectionStore.createReflection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null on AI session failure", async () => {
|
||||
const { agentStore, taskStore, reflectionStore } = createMockDeps();
|
||||
mockedCreateKbAgent.mockRejectedValue(new Error("AI unavailable"));
|
||||
mockedCreateFnAgent.mockRejectedValue(new Error("AI unavailable"));
|
||||
|
||||
const service = new AgentReflectionService({ agentStore, taskStore, reflectionStore, rootDir: tempRoot });
|
||||
const reflection = await service.generateReflection("agent-1", "manual");
|
||||
@@ -412,7 +412,7 @@ describe("AgentReflectionService", () => {
|
||||
const { agentStore, taskStore, reflectionStore } = createMockDeps();
|
||||
const session = createMockSession();
|
||||
|
||||
mockedCreateKbAgent.mockImplementation(async (options: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (options: any) => {
|
||||
options.onText?.(JSON.stringify({
|
||||
insights: ["Insight A"],
|
||||
suggestedImprovements: ["Improve B"],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -117,7 +117,7 @@ import { createFnAgent } from "./pi.js";
|
||||
import { execSync } from "node:child_process";
|
||||
import { type TaskStore, type Task, type MergeResult, DEFAULT_SETTINGS } from "@fusion/core";
|
||||
|
||||
const mockedCreateHaiAgent = vi.mocked(createFnAgent);
|
||||
const mockedCreateFnAgent = vi.mocked(createFnAgent);
|
||||
const mockedExecSync = vi.mocked(execSync);
|
||||
const { existsSync: mockedExistsSyncRaw, readFileSync: mockedReadFileSyncRaw } = await import("node:fs");
|
||||
const mockedExistsSync = vi.mocked(mockedExistsSyncRaw);
|
||||
@@ -246,7 +246,7 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
setupHappyPathExecSync();
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -333,7 +333,7 @@ describe("aiMergeTask — task.branch field", () => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
setupHappyPathExecSync();
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -402,7 +402,7 @@ describe("aiMergeTask — empty squash merge (branch already merged via dep)", (
|
||||
|
||||
expect(result.merged).toBe(true);
|
||||
// Agent should NOT have been spawned
|
||||
expect(mockedCreateHaiAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
// Task should still be moved to done
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
|
||||
});
|
||||
@@ -444,7 +444,7 @@ describe("push-after-merge", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -662,7 +662,7 @@ describe("push-after-merge", () => {
|
||||
let rebaseInProgress = false;
|
||||
let hasConflicts = false;
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(async () => {
|
||||
hasConflicts = false;
|
||||
@@ -709,7 +709,7 @@ describe("push-after-merge", () => {
|
||||
});
|
||||
|
||||
expect(result.pushed).toBe(true);
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retries push once after non-fast-forward rejection", async () => {
|
||||
@@ -755,7 +755,7 @@ describe("push-after-merge", () => {
|
||||
it("aborts rebase when conflicts remain unresolved", async () => {
|
||||
let rebaseInProgress = false;
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -808,7 +808,7 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
setupHappyPathExecSync();
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -824,7 +824,7 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
|
||||
|
||||
await aiMergeTask(store, "/tmp/root", "FN-050");
|
||||
|
||||
const agentCall = mockedCreateHaiAgent.mock.calls[0][0] as any;
|
||||
const agentCall = mockedCreateFnAgent.mock.calls[0][0] as any;
|
||||
expect(agentCall.systemPrompt).toContain("<type>(<scope>): <summary>");
|
||||
expect(agentCall.systemPrompt).toContain("the task ID");
|
||||
});
|
||||
@@ -841,7 +841,7 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
|
||||
|
||||
await aiMergeTask(store, "/tmp/root", "FN-050");
|
||||
|
||||
const agentCall = mockedCreateHaiAgent.mock.calls[0][0] as any;
|
||||
const agentCall = mockedCreateFnAgent.mock.calls[0][0] as any;
|
||||
expect(agentCall.systemPrompt).toContain("<type>: <summary>");
|
||||
expect(agentCall.systemPrompt).not.toContain("<type>(<scope>): <summary>");
|
||||
expect(agentCall.systemPrompt).toContain("Do NOT include a scope");
|
||||
@@ -917,7 +917,7 @@ describe("aiMergeTask — model settings threading", () => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
setupHappyPathExecSync();
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -938,8 +938,8 @@ describe("aiMergeTask — model settings threading", () => {
|
||||
|
||||
await aiMergeTask(store, "/tmp/root", "FN-050");
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0] as any;
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0] as any;
|
||||
expect(opts.defaultProvider).toBe("openai");
|
||||
expect(opts.defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
@@ -952,7 +952,7 @@ describe("aiMergeTask — model settings threading", () => {
|
||||
|
||||
await aiMergeTask(store, "/tmp/root", "FN-050");
|
||||
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0] as any;
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0] as any;
|
||||
expect(opts.defaultProvider).toBeUndefined();
|
||||
expect(opts.defaultModelId).toBeUndefined();
|
||||
});
|
||||
@@ -968,7 +968,7 @@ describe("aiMergeTask — agent log persistence", () => {
|
||||
it("logs text deltas to store.appendAgentLog", async () => {
|
||||
let capturedOnText: ((delta: string) => void) | undefined;
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedOnText = opts.onText;
|
||||
return {
|
||||
session: {
|
||||
@@ -995,7 +995,7 @@ describe("aiMergeTask — agent log persistence", () => {
|
||||
it("logs tool invocations to store.appendAgentLog", async () => {
|
||||
let capturedOnToolStart: ((name: string, args: any) => void) | undefined;
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedOnToolStart = opts.onToolStart;
|
||||
return {
|
||||
session: {
|
||||
@@ -1022,7 +1022,7 @@ describe("aiMergeTask — agent log persistence", () => {
|
||||
const onAgentText = vi.fn();
|
||||
let capturedOnText: ((delta: string) => void) | undefined;
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedOnText = opts.onText;
|
||||
return {
|
||||
session: {
|
||||
@@ -1068,7 +1068,7 @@ describe("aiMergeTask — usage limit detection", () => {
|
||||
const pauser = new UsageLimitPauser(store);
|
||||
const onUsageLimitHitSpy = vi.spyOn(pauser, "onUsageLimitHit");
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("rate_limit_error: Rate limit exceeded")),
|
||||
dispose: vi.fn(),
|
||||
@@ -1104,7 +1104,7 @@ describe("aiMergeTask — usage limit detection", () => {
|
||||
dispose: vi.fn(),
|
||||
state: { error: "429 Too Many Requests" },
|
||||
};
|
||||
mockedCreateHaiAgent.mockResolvedValue({ session: mockSession } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession } as any);
|
||||
|
||||
await expect(
|
||||
aiMergeTask(store, "/tmp/root", "FN-050", { usageLimitPauser: pauser }),
|
||||
@@ -1131,7 +1131,7 @@ describe("aiMergeTask — usage limit detection", () => {
|
||||
const pauser = new UsageLimitPauser(store);
|
||||
const onUsageLimitHitSpy = vi.spyOn(pauser, "onUsageLimitHit");
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("connection refused")),
|
||||
dispose: vi.fn(),
|
||||
@@ -1151,7 +1151,7 @@ describe("aiMergeTask — usage limit detection", () => {
|
||||
[{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
|
||||
);
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("rate_limit_error: Rate limit exceeded")),
|
||||
dispose: vi.fn(),
|
||||
@@ -1172,7 +1172,7 @@ describe("aiMergeTask — usage limit detection", () => {
|
||||
const pauser = new UsageLimitPauser(store);
|
||||
const onUsageLimitHitSpy = vi.spyOn(pauser, "onUsageLimitHit");
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("overloaded_error: Overloaded")),
|
||||
dispose: vi.fn(),
|
||||
@@ -1204,7 +1204,7 @@ describe("aiMergeTask — onSession callback", () => {
|
||||
dispose: vi.fn(),
|
||||
};
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession,
|
||||
} as any);
|
||||
|
||||
@@ -1221,7 +1221,7 @@ describe("aiMergeTask — onSession callback", () => {
|
||||
});
|
||||
|
||||
it("works without onSession callback (backward compatible)", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -1594,7 +1594,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -1680,7 +1680,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
|
||||
});
|
||||
|
||||
// Agent will be called and will fail
|
||||
mockedCreateHaiAgent.mockImplementation(() => {
|
||||
mockedCreateFnAgent.mockImplementation(() => {
|
||||
agentCallCount++;
|
||||
return Promise.resolve({
|
||||
session: {
|
||||
@@ -1796,7 +1796,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
|
||||
});
|
||||
|
||||
// Agent fails on attempt 2 (when called to resolve complex conflicts)
|
||||
mockedCreateHaiAgent.mockImplementation(() => {
|
||||
mockedCreateFnAgent.mockImplementation(() => {
|
||||
agentCallCount++;
|
||||
if (agentCallCount === 1) {
|
||||
// First agent call (attempt 2) fails
|
||||
@@ -1873,7 +1873,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
|
||||
});
|
||||
|
||||
// Agent will also fail
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("Agent failed")),
|
||||
dispose: vi.fn(),
|
||||
@@ -1936,7 +1936,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("Agent failed")),
|
||||
dispose: vi.fn(),
|
||||
@@ -2062,7 +2062,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error(buildFailureMessage)),
|
||||
dispose: vi.fn(),
|
||||
@@ -2154,7 +2154,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("Agent failed on attempt 2")),
|
||||
dispose: vi.fn(),
|
||||
@@ -2268,7 +2268,7 @@ describe("aiMergeTask — reset cleanup failure diagnostics", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("Agent failed")),
|
||||
dispose: vi.fn(),
|
||||
@@ -2342,7 +2342,7 @@ describe("aiMergeTask — reset cleanup failure diagnostics", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("git merge failed: exit code 128")),
|
||||
dispose: vi.fn(),
|
||||
@@ -2385,7 +2385,7 @@ describe("aiMergeTask — reset cleanup failure diagnostics", () => {
|
||||
const warnSpy = vi.spyOn(mergerLog, "warn");
|
||||
const resetFailureMessage = "lock file busy";
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
const reportTool = opts.customTools?.find((t: any) => t.name === "report_build_failure");
|
||||
return {
|
||||
session: {
|
||||
@@ -2755,7 +2755,7 @@ describe("aiMergeTask — build verification", () => {
|
||||
|
||||
it("system prompt contains build verification section", async () => {
|
||||
let capturedSystemPrompt: string | undefined;
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedSystemPrompt = opts.systemPrompt;
|
||||
return {
|
||||
session: {
|
||||
@@ -2777,7 +2777,7 @@ describe("aiMergeTask — build verification", () => {
|
||||
it("includes build command in merge prompt when configured", async () => {
|
||||
let capturedArgs: any;
|
||||
let capturedPrompt: string | undefined;
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedArgs = opts;
|
||||
// Simulate agent committing by returning session that results in clean state
|
||||
return {
|
||||
@@ -2825,7 +2825,7 @@ describe("aiMergeTask — build verification", () => {
|
||||
});
|
||||
|
||||
it("merge succeeds when build passes (agent reports success)", async () => {
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
@@ -2866,7 +2866,7 @@ describe("aiMergeTask — build verification", () => {
|
||||
|
||||
it("merge aborts when build fails via report_build_failure tool", async () => {
|
||||
// Mock agent that calls the report_build_failure tool execute method
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
const reportTool = opts.customTools?.find((t: any) => t.name === "report_build_failure");
|
||||
return {
|
||||
session: {
|
||||
@@ -2941,7 +2941,7 @@ describe("aiMergeTask — build verification", () => {
|
||||
const warnSpy = vi.spyOn(mergerLog, "warn");
|
||||
const resetFailureMessage = "reset failed: dirty working tree";
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
const reportTool = opts.customTools?.find((t: any) => t.name === "report_build_failure");
|
||||
return {
|
||||
session: {
|
||||
@@ -2982,7 +2982,7 @@ describe("aiMergeTask — build verification", () => {
|
||||
});
|
||||
|
||||
it("merge proceeds normally when no build command is configured", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -3002,7 +3002,7 @@ describe("aiMergeTask — build verification", () => {
|
||||
});
|
||||
|
||||
it("merge proceeds when buildCommand is empty string (treated as undefined)", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -3025,7 +3025,7 @@ describe("aiMergeTask — build verification", () => {
|
||||
});
|
||||
|
||||
it("syncs dependencies before build verification when install state is missing", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -3134,7 +3134,7 @@ describe("aiMergeTask — deterministic merge verification", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
@@ -3204,7 +3204,7 @@ describe("aiMergeTask — deterministic merge verification", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -3275,7 +3275,7 @@ describe("aiMergeTask — deterministic merge verification", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async () => ({
|
||||
mockedCreateFnAgent.mockImplementation(async () => ({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -3318,7 +3318,7 @@ describe("aiMergeTask — deterministic merge verification", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
@@ -3392,7 +3392,7 @@ describe("aiMergeTask — deterministic merge verification", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
@@ -3451,7 +3451,7 @@ describe("aiMergeTask — deterministic merge verification", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
@@ -3683,7 +3683,7 @@ describe("aiMergeTask — post-merge workflow steps", () => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
setupHappyPathExecSync();
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -3773,10 +3773,10 @@ describe("aiMergeTask — post-merge workflow steps", () => {
|
||||
// getWorkflowStep may be called but pre-merge steps should not trigger agent creation
|
||||
// beyond the merge agent itself. We verify createFnAgent was called only once (merge agent)
|
||||
// since pre-merge steps are skipped in the merger
|
||||
const mergeAgentCalls = mockedCreateHaiAgent.mock.calls.filter(
|
||||
const mergeAgentCalls = mockedCreateFnAgent.mock.calls.filter(
|
||||
(c: any) => c[0]?.systemPrompt?.includes("You are a merge agent")
|
||||
);
|
||||
const postMergeCalls = mockedCreateHaiAgent.mock.calls.filter(
|
||||
const postMergeCalls = mockedCreateFnAgent.mock.calls.filter(
|
||||
(c: any) => c[0]?.systemPrompt?.includes("post-merge")
|
||||
);
|
||||
|
||||
@@ -3870,7 +3870,7 @@ describe("aiMergeTask — post-merge workflow steps", () => {
|
||||
store.getTask = vi.fn().mockResolvedValue({ ...baseTask, prompt: "# test" });
|
||||
|
||||
// Make the post-merge agent throw
|
||||
mockedCreateHaiAgent.mockImplementation((async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation((async (opts: any) => {
|
||||
if (opts.systemPrompt?.includes("post-merge")) {
|
||||
return {
|
||||
session: {
|
||||
@@ -3968,7 +3968,7 @@ describe("aiMergeTask — merge details collection", () => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -4173,7 +4173,7 @@ describe("aiMergeTask — fresh session and compaction recovery", () => {
|
||||
setupFreshSessionExecSync();
|
||||
|
||||
const sessionInstances: any[] = [];
|
||||
mockedCreateHaiAgent.mockImplementation(async () => {
|
||||
mockedCreateFnAgent.mockImplementation(async () => {
|
||||
const session = {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -4191,7 +4191,7 @@ describe("aiMergeTask — fresh session and compaction recovery", () => {
|
||||
await aiMergeTask(store, "/tmp/root", "FN-050");
|
||||
|
||||
// Session should be created once for the merge agent
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
expect(sessionInstances.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -4203,7 +4203,7 @@ describe("aiMergeTask — fresh session and compaction recovery", () => {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: mockDispose,
|
||||
};
|
||||
mockedCreateHaiAgent.mockResolvedValue({ session: mockSession } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession } as any);
|
||||
|
||||
const store = createMockStore(
|
||||
{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050" },
|
||||
@@ -4397,7 +4397,7 @@ describe("aiMergeTask — context limit recovery with truncation", () => {
|
||||
// Track prompt calls
|
||||
const promptCalls: string[] = [];
|
||||
let firstCall = true;
|
||||
mockedCreateHaiAgent.mockImplementation(async () => {
|
||||
mockedCreateFnAgent.mockImplementation(async () => {
|
||||
const session = {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
promptCalls.push(prompt);
|
||||
@@ -4445,7 +4445,7 @@ describe("aiMergeTask — context limit recovery with truncation", () => {
|
||||
// Track prompt calls to verify both original and truncated prompts were tried
|
||||
const promptCalls: string[] = [];
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async () => {
|
||||
mockedCreateFnAgent.mockImplementation(async () => {
|
||||
const session = {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
promptCalls.push(prompt);
|
||||
@@ -4507,7 +4507,7 @@ describe("aiMergeTask — context limit recovery with truncation", () => {
|
||||
// Track prompt calls
|
||||
const promptCalls: string[] = [];
|
||||
let firstCall = true;
|
||||
mockedCreateHaiAgent.mockImplementation(async () => {
|
||||
mockedCreateFnAgent.mockImplementation(async () => {
|
||||
const session = {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
promptCalls.push(prompt);
|
||||
@@ -4549,7 +4549,7 @@ describe("aiMergeTask — context limit recovery with truncation", () => {
|
||||
vi.mocked(isContextLimitError).mockReturnValue(false);
|
||||
|
||||
// Mock non-context error
|
||||
mockedCreateHaiAgent.mockImplementation(async () => {
|
||||
mockedCreateFnAgent.mockImplementation(async () => {
|
||||
const session = {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("connection refused")),
|
||||
dispose: vi.fn(),
|
||||
@@ -4789,7 +4789,7 @@ describe("aiMergeTask — inferred test command execution", () => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
setupHappyPathExecSync();
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -5038,7 +5038,7 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
skillSource: "role-fallback",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
@@ -5063,9 +5063,9 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
agentStore: mockAgentStore as any,
|
||||
});
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
// Find the first createFnAgent call (main merger agent)
|
||||
const firstCall = mockedCreateHaiAgent.mock.calls[0];
|
||||
const firstCall = mockedCreateFnAgent.mock.calls[0];
|
||||
const opts = firstCall[0];
|
||||
expect(opts.skillSelection).toBeDefined();
|
||||
expect(opts.skillSelection!.projectRootDir).toBe("/tmp/root");
|
||||
@@ -5085,7 +5085,7 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
skillSource: "assigned-agent",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
@@ -5110,8 +5110,8 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
agentStore: mockAgentStore as any,
|
||||
});
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateFnAgent.mock.calls[0];
|
||||
const opts = firstCall[0];
|
||||
expect(opts.skillSelection).toBeDefined();
|
||||
expect(opts.skillSelection!.requestedSkillNames).toEqual(["custom-skill", "another-skill"]);
|
||||
@@ -5125,7 +5125,7 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
skillSource: "none",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
@@ -5150,15 +5150,15 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
agentStore: mockAgentStore as any,
|
||||
});
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateFnAgent.mock.calls[0];
|
||||
const opts = firstCall[0];
|
||||
// skillSelection should not be present when context is undefined
|
||||
expect("skillSelection" in opts).toBe(false);
|
||||
});
|
||||
|
||||
it("does not pass skillSelection when agentStore is not provided", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
@@ -5178,8 +5178,8 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
// No agentStore provided
|
||||
await aiMergeTask(store, "/tmp/root", "FN-050");
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateFnAgent.mock.calls[0];
|
||||
const opts = firstCall[0];
|
||||
expect("skillSelection" in opts).toBe(false);
|
||||
});
|
||||
@@ -5188,7 +5188,7 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
const { buildSessionSkillContext } = await import("./session-skill-context.js");
|
||||
vi.mocked(buildSessionSkillContext).mockRejectedValue(new Error("Agent not found"));
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
@@ -5215,8 +5215,8 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
});
|
||||
|
||||
expect(result.merged).toBe(true);
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateFnAgent.mock.calls[0];
|
||||
const opts = firstCall[0];
|
||||
expect("skillSelection" in opts).toBe(false);
|
||||
});
|
||||
@@ -5234,7 +5234,7 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
skillSource: "assigned-agent",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
@@ -5259,8 +5259,8 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
agentStore: mockAgentStore as any,
|
||||
});
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateFnAgent.mock.calls[0];
|
||||
const opts = firstCall[0];
|
||||
expect(opts.skillSelection?.requestedSkillNames).toEqual(resolvedNames);
|
||||
});
|
||||
@@ -5277,7 +5277,7 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
skillSource: "role-fallback",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
@@ -5302,8 +5302,8 @@ describe("aiMergeTask — skill selection resolver contract (FN-1510/FN-1511)",
|
||||
agentStore: mockAgentStore as any,
|
||||
});
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateFnAgent.mock.calls[0];
|
||||
const opts = firstCall[0];
|
||||
expect(opts.skillSelection?.sessionPurpose).toBe("merger");
|
||||
});
|
||||
@@ -5338,7 +5338,7 @@ describe("aiMergeTask — skill selection non-fatal diagnostics (FN-1510/FN-1511
|
||||
skillSource: "none",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
@@ -5380,7 +5380,7 @@ describe("aiMergeTask — skill selection non-fatal diagnostics (FN-1510/FN-1511
|
||||
skillSource: "assigned-agent",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
@@ -5409,8 +5409,8 @@ describe("aiMergeTask — skill selection non-fatal diagnostics (FN-1510/FN-1511
|
||||
expect(result.merged).toBe(true);
|
||||
|
||||
// Verify skillSelection was passed with the custom skill
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||
const firstCall = mockedCreateFnAgent.mock.calls[0];
|
||||
const opts = firstCall[0];
|
||||
expect(opts.skillSelection?.requestedSkillNames).toEqual(["custom-skill"]);
|
||||
});
|
||||
@@ -5447,7 +5447,7 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
const isFixAgent = opts.systemPrompt?.includes("verification fix agent");
|
||||
return {
|
||||
session: {
|
||||
@@ -5474,10 +5474,10 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
});
|
||||
|
||||
// Verify that fix agent was spawned (2 calls: merger + fix)
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Verify the fix agent was called with correct options
|
||||
const fixAgentCall = mockedCreateHaiAgent.mock.calls[1];
|
||||
const fixAgentCall = mockedCreateFnAgent.mock.calls[1];
|
||||
expect(fixAgentCall[0].tools).toBe("coding");
|
||||
expect(fixAgentCall[0].cwd).toBe("/tmp/root");
|
||||
});
|
||||
@@ -5505,7 +5505,7 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -5527,7 +5527,7 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
});
|
||||
|
||||
// Verify fix agent was NOT spawned (only merger)
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Verify no fix attempt was logged
|
||||
const logCalls = (store.logEntry as ReturnType<typeof vi.fn>).mock.calls;
|
||||
@@ -5560,7 +5560,7 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -5586,7 +5586,7 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
});
|
||||
|
||||
// Verify fix agent uses same model settings
|
||||
const fixAgentCall = mockedCreateHaiAgent.mock.calls[1];
|
||||
const fixAgentCall = mockedCreateFnAgent.mock.calls[1];
|
||||
expect(fixAgentCall[0].defaultProvider).toBe("anthropic");
|
||||
expect(fixAgentCall[0].defaultModelId).toBe("claude-sonnet-4-5");
|
||||
});
|
||||
@@ -5616,7 +5616,7 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
const isFixAgent = opts.systemPrompt?.includes("verification fix agent");
|
||||
if (isFixAgent) {
|
||||
return {
|
||||
@@ -5674,7 +5674,7 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -5698,6 +5698,6 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
});
|
||||
|
||||
// Should have 3 fix attempts (capped at 3) + 1 merger = 4 calls
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(4);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -734,9 +734,9 @@ describe("createFnAgent", () => {
|
||||
},
|
||||
}));
|
||||
|
||||
const { createFnAgent: freshCreateKbAgent } = await import("./pi.js");
|
||||
const { createFnAgent: freshCreateFnAgent } = await import("./pi.js");
|
||||
|
||||
await freshCreateKbAgent({
|
||||
await freshCreateFnAgent({
|
||||
cwd: "/tmp",
|
||||
systemPrompt: "test",
|
||||
tools: "coding",
|
||||
@@ -809,9 +809,9 @@ describe("createFnAgent", () => {
|
||||
},
|
||||
}));
|
||||
|
||||
const { createFnAgent: freshCreateKbAgent } = await import("./pi.js");
|
||||
const { createFnAgent: freshCreateFnAgent } = await import("./pi.js");
|
||||
|
||||
await freshCreateKbAgent({
|
||||
await freshCreateFnAgent({
|
||||
cwd: "/tmp",
|
||||
systemPrompt: "test",
|
||||
tools: "coding",
|
||||
@@ -881,9 +881,9 @@ describe("createFnAgent", () => {
|
||||
},
|
||||
}));
|
||||
|
||||
const { createFnAgent: freshCreateKbAgent } = await import("./pi.js");
|
||||
const { createFnAgent: freshCreateFnAgent } = await import("./pi.js");
|
||||
|
||||
await freshCreateKbAgent({
|
||||
await freshCreateFnAgent({
|
||||
cwd: "/tmp",
|
||||
systemPrompt: "test",
|
||||
tools: "coding",
|
||||
|
||||
@@ -101,7 +101,7 @@ import { execSync } from "node:child_process";
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import type { Task, TaskDetail, TaskStep, Column, Settings, StepStatus } from "@fusion/core";
|
||||
|
||||
const mockedCreateHaiAgent = vi.mocked(createFnAgent);
|
||||
const mockedCreateFnAgent = vi.mocked(createFnAgent);
|
||||
const mockedExecSync = vi.mocked(execSync);
|
||||
const mockedExistsSync = vi.mocked(existsSync);
|
||||
const mockedReaddirSync = vi.mocked(readdirSync);
|
||||
@@ -189,7 +189,7 @@ function makeSteps(...statuses: StepStatus[]): TaskStep[] {
|
||||
}
|
||||
|
||||
function mockAgentSuccess() {
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -198,7 +198,7 @@ function mockAgentSuccess() {
|
||||
}
|
||||
|
||||
function mockAgentFailure(error = "agent crashed") {
|
||||
mockedCreateHaiAgent.mockRejectedValue(new Error(error));
|
||||
mockedCreateFnAgent.mockRejectedValue(new Error(error));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,7 +211,7 @@ function mockAgentFailure(error = "agent crashed") {
|
||||
* multiple tasks execute concurrently.
|
||||
*/
|
||||
function createAgentWithTaskDone() {
|
||||
mockedCreateHaiAgent.mockImplementation((async (opts: { customTools?: Array<{ name: string; execute: (name: string, args: unknown) => unknown }> }) => {
|
||||
mockedCreateFnAgent.mockImplementation((async (opts: { customTools?: Array<{ name: string; execute: (name: string, args: unknown) => unknown }> }) => {
|
||||
// Capture tools per-session to avoid race conditions with concurrent tasks
|
||||
const localCustomTools = opts?.customTools || [];
|
||||
const session = {
|
||||
@@ -286,7 +286,7 @@ describe("In-progress task resume after restart", () => {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
// Exactly one agent session per in-progress task (no retry inflation)
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Both tasks should have received resume log entries (behavioral guarantee)
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-001", "Resumed after engine restart");
|
||||
@@ -329,7 +329,7 @@ describe("In-progress task resume after restart", () => {
|
||||
}));
|
||||
|
||||
let capturedPrompt = "";
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
capturedPrompt = prompt;
|
||||
@@ -467,7 +467,7 @@ describe("In-progress task resume after restart", () => {
|
||||
await executor.resumeOrphaned();
|
||||
|
||||
expect(executeSpy).not.toHaveBeenCalled();
|
||||
expect(mockedCreateHaiAgent).not.toHaveBeenCalled();
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
expect(store.logEntry).not.toHaveBeenCalledWith("FN-1473", "Resumed after engine restart");
|
||||
});
|
||||
|
||||
@@ -627,7 +627,7 @@ describe("In-review merge handling after restart", () => {
|
||||
return Buffer.from("");
|
||||
}) as any);
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("merge agent crashed")),
|
||||
dispose: vi.fn(),
|
||||
@@ -695,7 +695,7 @@ describe("Triage re-pick after restart", () => {
|
||||
// Both triage tasks should have been picked up for specification
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-060", { status: "specifying" });
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-061", { status: "specifying" });
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("specifyTask() skips task already in processing set (no double-specification)", async () => {
|
||||
@@ -705,7 +705,7 @@ describe("Triage re-pick after restart", () => {
|
||||
|
||||
// Slow agent to keep task in processing
|
||||
let resolvePrompt: (() => void) | undefined;
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(() => new Promise<void>((r) => { resolvePrompt = r; })),
|
||||
dispose: vi.fn(),
|
||||
@@ -724,7 +724,7 @@ describe("Triage re-pick after restart", () => {
|
||||
await triage.specifyTask(task);
|
||||
|
||||
// Only one agent created
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Resolve the blocked prompt to clean up
|
||||
resolvePrompt!();
|
||||
@@ -863,7 +863,7 @@ describe("Crash scenario edge cases", () => {
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("FN-090", "in-progress"));
|
||||
|
||||
// Agent session.prompt rejects (simulating crash mid-step)
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("agent died mid-step")),
|
||||
dispose: vi.fn(),
|
||||
@@ -898,7 +898,7 @@ describe("Crash scenario edge cases", () => {
|
||||
|
||||
// Exactly one agent created for the re-resume, proving the task was eligible
|
||||
// and completed without retry inflation.
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Re-resume should have logged again (behavioral guarantee)
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-090", "Resumed after engine restart");
|
||||
@@ -929,7 +929,7 @@ describe("Crash scenario edge cases", () => {
|
||||
}) as any);
|
||||
|
||||
// Agent prompt rejects (simulating kill during merge)
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockRejectedValue(new Error("killed")),
|
||||
dispose: vi.fn(),
|
||||
@@ -958,7 +958,7 @@ describe("Crash scenario edge cases", () => {
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("FN-092", "in-progress"));
|
||||
|
||||
let resolvePrompt: (() => void) | undefined;
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(() => new Promise<void>((r) => { resolvePrompt = r; })),
|
||||
dispose: vi.fn(),
|
||||
@@ -976,7 +976,7 @@ describe("Crash scenario edge cases", () => {
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
|
||||
// Only one agent should have been created (the executing set guards against double-exec)
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clean up
|
||||
resolvePrompt!();
|
||||
@@ -997,7 +997,7 @@ describe("Crash scenario edge cases", () => {
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("FN-093", "in-progress"));
|
||||
|
||||
// Agent creation itself fails
|
||||
mockedCreateHaiAgent.mockRejectedValue(new Error("cannot create agent"));
|
||||
mockedCreateFnAgent.mockRejectedValue(new Error("cannot create agent"));
|
||||
|
||||
const onError = vi.fn();
|
||||
const executor = new TaskExecutor(store, "/tmp/test", {
|
||||
@@ -1260,7 +1260,7 @@ describe("Engine pause/unpause cycle", () => {
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("FN-EP1", "in-progress"));
|
||||
|
||||
// Agent triggers engine pause mid-flight but continues normally (soft pause)
|
||||
mockedCreateHaiAgent.mockImplementation(async () => ({
|
||||
mockedCreateFnAgent.mockImplementation(async () => ({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
// Trigger engine pause — session should NOT be terminated
|
||||
@@ -1289,7 +1289,7 @@ describe("Engine pause/unpause cycle", () => {
|
||||
let sessionContinued = false;
|
||||
|
||||
// Agent triggers engine pause mid-flight; session should NOT be disposed
|
||||
mockedCreateHaiAgent.mockImplementation(async () => ({
|
||||
mockedCreateFnAgent.mockImplementation(async () => ({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
store._trigger("settings:updated", {
|
||||
@@ -1368,7 +1368,7 @@ describe("Engine pause/unpause cycle", () => {
|
||||
const store = createMockStore();
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("FN-EP5", "in-progress"));
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async () => ({
|
||||
mockedCreateFnAgent.mockImplementation(async () => ({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
// Trigger engine pause while the agent holds the semaphore slot
|
||||
|
||||
@@ -15,7 +15,7 @@ vi.mock("./pi.js", () => ({
|
||||
import { reviewStep, REVIEWER_SYSTEM_PROMPT } from "./reviewer.js";
|
||||
import { createFnAgent } from "./pi.js";
|
||||
|
||||
const mockedCreateHaiAgent = vi.mocked(createFnAgent);
|
||||
const mockedCreateFnAgent = vi.mocked(createFnAgent);
|
||||
|
||||
function createMockSession(reviewText: string) {
|
||||
return {
|
||||
@@ -39,7 +39,7 @@ describe("reviewStep — model settings threading", () => {
|
||||
});
|
||||
|
||||
it("passes defaultProvider and defaultModelId to createFnAgent when provided", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -52,14 +52,14 @@ describe("reviewStep — model settings threading", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("anthropic");
|
||||
expect(opts.defaultModelId).toBe("claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("does not set model fields when ReviewOptions omits them", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nAll good."),
|
||||
);
|
||||
|
||||
@@ -69,14 +69,14 @@ describe("reviewStep — model settings threading", () => {
|
||||
{},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBeUndefined();
|
||||
expect(opts.defaultModelId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("extracts APPROVE verdict correctly", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("reviewStep — spec review type", () => {
|
||||
});
|
||||
|
||||
it("extracts verdict correctly for spec reviews", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("## Spec Review: KB-050\n\n### Verdict: APPROVE\n### Summary\nSpec looks complete and well-structured."),
|
||||
);
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("reviewStep — spec review type", () => {
|
||||
});
|
||||
|
||||
it("extracts REVISE verdict for spec reviews", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("## Spec Review: KB-050\n\n### Verdict: REVISE\n### Summary\nMissing test requirements."),
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ describe("reviewStep — spec review type", () => {
|
||||
});
|
||||
|
||||
it("extracts RETHINK verdict for spec reviews", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("## Spec Review: KB-050\n\n### Verdict: RETHINK\n### Summary\nFundamentally wrong approach."),
|
||||
);
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("reviewStep — spec review type", () => {
|
||||
});
|
||||
|
||||
it("calls createFnAgent with readonly tools and correct system prompt", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood spec."),
|
||||
);
|
||||
|
||||
@@ -139,15 +139,15 @@ describe("reviewStep — spec review type", () => {
|
||||
"/tmp/worktree", "FN-050", 0, "Spec Review", "spec", "# Task: KB-050",
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.tools).toBe("readonly");
|
||||
expect(opts.systemPrompt).toContain("Spec Review Format");
|
||||
expect(opts.systemPrompt).toContain("Mission clarity");
|
||||
});
|
||||
|
||||
it("injects read-only memory instructions and tools when project memory is enabled", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood spec."),
|
||||
);
|
||||
|
||||
@@ -157,14 +157,14 @@ describe("reviewStep — spec review type", () => {
|
||||
{ rootDir: "/tmp/project", settings: { memoryBackendType: "qmd" } as any },
|
||||
);
|
||||
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.systemPrompt).toContain("## Project Memory");
|
||||
expect(opts.systemPrompt).toContain("Do not update memory during review");
|
||||
expect(opts.customTools?.map((tool: any) => tool.name)).toEqual(["memory_search", "memory_get"]);
|
||||
});
|
||||
|
||||
it("omits reviewer memory tools and instructions when memory is disabled", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood spec."),
|
||||
);
|
||||
|
||||
@@ -174,14 +174,14 @@ describe("reviewStep — spec review type", () => {
|
||||
{ rootDir: "/tmp/project", settings: { memoryEnabled: false } as any },
|
||||
);
|
||||
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.systemPrompt).not.toContain("## Project Memory");
|
||||
expect(opts.customTools).toBeUndefined();
|
||||
});
|
||||
|
||||
it("builds review request with spec-specific instructions", async () => {
|
||||
let capturedPrompt = "";
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
capturedPrompt = prompt;
|
||||
@@ -210,7 +210,7 @@ describe("reviewStep — spec review type", () => {
|
||||
|
||||
it("does not include git diff instructions for spec reviews", async () => {
|
||||
let capturedPrompt = "";
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
capturedPrompt = prompt;
|
||||
@@ -249,7 +249,7 @@ describe("reviewStep — exhausted-retry error detection", () => {
|
||||
dispose: vi.fn(),
|
||||
state: { error: "rate_limit_error: Rate limit exceeded" },
|
||||
};
|
||||
mockedCreateHaiAgent.mockResolvedValue({ session: mockSession } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession } as any);
|
||||
|
||||
await expect(
|
||||
reviewStep("/tmp/worktree", "FN-100", 1, "Test Step", "code", "# prompt"),
|
||||
@@ -264,7 +264,7 @@ describe("reviewStep — exhausted-retry error detection", () => {
|
||||
dispose: disposeFn,
|
||||
state: { error: "rate_limit_error: Rate limit exceeded" },
|
||||
};
|
||||
mockedCreateHaiAgent.mockResolvedValue({ session: mockSession } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession } as any);
|
||||
|
||||
await expect(
|
||||
reviewStep("/tmp/worktree", "FN-100", 1, "Test Step", "code", "# prompt"),
|
||||
@@ -275,7 +275,7 @@ describe("reviewStep — exhausted-retry error detection", () => {
|
||||
});
|
||||
|
||||
it("does not throw when session completes without error", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -293,7 +293,7 @@ describe("reviewStep — validator model overrides", () => {
|
||||
});
|
||||
|
||||
it("uses taskValidatorProvider and taskValidatorModelId when both are set", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -308,14 +308,14 @@ describe("reviewStep — validator model overrides", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("anthropic");
|
||||
expect(opts.defaultModelId).toBe("claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("falls back to defaultProvider/defaultModelId when taskValidatorProvider is missing", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -330,14 +330,14 @@ describe("reviewStep — validator model overrides", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("openai");
|
||||
expect(opts.defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
it("falls back to defaultProvider/defaultModelId when taskValidatorModelId is missing", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -352,14 +352,14 @@ describe("reviewStep — validator model overrides", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("openai");
|
||||
expect(opts.defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
it("falls back to defaultProvider/defaultModelId when both validator fields are undefined", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -372,14 +372,14 @@ describe("reviewStep — validator model overrides", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("openai");
|
||||
expect(opts.defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
it("resolves project validator override when task override is not set", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -395,14 +395,14 @@ describe("reviewStep — validator model overrides", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("anthropic");
|
||||
expect(opts.defaultModelId).toBe("claude-opus-4");
|
||||
});
|
||||
|
||||
it("resolves global validator lane when project override is not set", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -419,14 +419,14 @@ describe("reviewStep — validator model overrides", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("google");
|
||||
expect(opts.defaultModelId).toBe("gemini-2.5");
|
||||
});
|
||||
|
||||
it("falls back to execution default when no validator lanes are set", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
@@ -440,8 +440,8 @@ describe("reviewStep — validator model overrides", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("openai");
|
||||
expect(opts.defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
@@ -488,10 +488,10 @@ describe("REVIEWER_SYSTEM_PROMPT", () => {
|
||||
});
|
||||
|
||||
describe("reviewStep — user comments in spec review", () => {
|
||||
let mockedCreateHaiAgent: ReturnType<typeof vi.fn>;
|
||||
let mockedCreateFnAgent: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedCreateHaiAgent = vi.fn().mockResolvedValue({
|
||||
mockedCreateFnAgent = vi.fn().mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
subscribe: vi.fn().mockImplementation((cb: any) => {
|
||||
@@ -504,12 +504,12 @@ describe("reviewStep — user comments in spec review", () => {
|
||||
sessionManager: { getLeafId: vi.fn() },
|
||||
},
|
||||
} as any);
|
||||
vi.mocked(createFnAgent).mockImplementation(mockedCreateHaiAgent);
|
||||
vi.mocked(createFnAgent).mockImplementation(mockedCreateFnAgent);
|
||||
});
|
||||
|
||||
it("includes user comments in spec review request", async () => {
|
||||
let capturedPrompt = "";
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
capturedPrompt = prompt;
|
||||
@@ -547,7 +547,7 @@ describe("reviewStep — user comments in spec review", () => {
|
||||
|
||||
it("does not include user comments section when no comments provided", async () => {
|
||||
let capturedPrompt = "";
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
capturedPrompt = prompt;
|
||||
@@ -572,7 +572,7 @@ describe("reviewStep — user comments in spec review", () => {
|
||||
|
||||
it("does not include user comments for non-spec review types", async () => {
|
||||
let capturedPrompt = "";
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
capturedPrompt = prompt;
|
||||
@@ -609,7 +609,7 @@ describe("reviewStep — user comments in spec review", () => {
|
||||
|
||||
it("includes assigned worktree boundary instructions for code reviews", async () => {
|
||||
let capturedPrompt = "";
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async (prompt: string) => {
|
||||
capturedPrompt = prompt;
|
||||
@@ -659,7 +659,7 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
skillSource: "role-fallback",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood."),
|
||||
);
|
||||
|
||||
@@ -676,8 +676,8 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.skillSelection).toBeDefined();
|
||||
expect(opts.skillSelection!.projectRootDir).toBe("/tmp/project");
|
||||
expect(opts.skillSelection!.requestedSkillNames).toEqual(["reviewer"]);
|
||||
@@ -696,7 +696,7 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
skillSource: "assigned-agent",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood."),
|
||||
);
|
||||
|
||||
@@ -714,8 +714,8 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.skillSelection).toBeDefined();
|
||||
expect(opts.skillSelection!.requestedSkillNames).toEqual(["custom-skill", "another-skill"]);
|
||||
expect(opts.skillSelection!.sessionPurpose).toBe("reviewer");
|
||||
@@ -729,7 +729,7 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
skillSource: "none",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood."),
|
||||
);
|
||||
|
||||
@@ -746,15 +746,15 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
// skillSelection should not be present when context is undefined
|
||||
expect("skillSelection" in opts).toBe(false);
|
||||
});
|
||||
|
||||
it("does not pass skillSelection when agentStore or rootDir is missing", async () => {
|
||||
// Without agentStore/rootDir, buildSessionSkillContext is never called
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood."),
|
||||
);
|
||||
|
||||
@@ -766,8 +766,8 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect("skillSelection" in opts).toBe(false);
|
||||
});
|
||||
|
||||
@@ -775,7 +775,7 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
const { buildSessionSkillContext } = await import("./session-skill-context.js");
|
||||
vi.mocked(buildSessionSkillContext).mockRejectedValue(new Error("Agent not found"));
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood."),
|
||||
);
|
||||
|
||||
@@ -793,8 +793,8 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect("skillSelection" in opts).toBe(false);
|
||||
});
|
||||
|
||||
@@ -811,7 +811,7 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
skillSource: "assigned-agent",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood."),
|
||||
);
|
||||
|
||||
@@ -829,8 +829,8 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
);
|
||||
|
||||
// Verify the resolved names are passed to createFnAgent
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.skillSelection?.requestedSkillNames).toEqual(resolvedNames);
|
||||
});
|
||||
|
||||
@@ -846,7 +846,7 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
skillSource: "role-fallback",
|
||||
});
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
mockedCreateFnAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nGood."),
|
||||
);
|
||||
|
||||
@@ -863,8 +863,8 @@ describe("reviewStep — skill selection resolver contract (FN-1510/FN-1511)", (
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateFnAgent.mock.calls[0][0];
|
||||
expect(opts.skillSelection?.sessionPurpose).toBe("reviewer");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -645,7 +645,7 @@ import { execSync } from "node:child_process";
|
||||
import { AgentSemaphore } from "./concurrency.js";
|
||||
import { createLogger } from "./logger.js";
|
||||
|
||||
const mockedCreateKbAgent = vi.mocked(createFnAgent);
|
||||
const mockedCreateFnAgent = vi.mocked(createFnAgent);
|
||||
const mockedExecSync = vi.mocked(execSync);
|
||||
const mockedGenerateWorktreeName = vi.mocked(generateWorktreeName);
|
||||
const mockedCreateLogger = vi.mocked(createLogger);
|
||||
@@ -707,7 +707,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 1 });
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
|
||||
const onStepStart = vi.fn();
|
||||
const onStepComplete = vi.fn();
|
||||
@@ -728,7 +728,7 @@ describe("StepSessionExecutor", () => {
|
||||
expect(results.every((r) => r.retries === 0)).toBe(true);
|
||||
|
||||
// Verify 3 sessions were created
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledTimes(3);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Verify callbacks
|
||||
expect(onStepStart).toHaveBeenCalledTimes(3);
|
||||
@@ -755,7 +755,7 @@ describe("StepSessionExecutor", () => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
taskDetail: task,
|
||||
@@ -779,7 +779,7 @@ describe("StepSessionExecutor", () => {
|
||||
});
|
||||
|
||||
// 3 sessions created for step 0 (2 failures + 1 success) + 1 for step 1
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledTimes(4);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("step failure after max retries: returns failure result", async () => {
|
||||
@@ -796,7 +796,7 @@ describe("StepSessionExecutor", () => {
|
||||
const successSession = makeMockSession();
|
||||
|
||||
let createCount = 0;
|
||||
mockedCreateKbAgent.mockImplementation(() => {
|
||||
mockedCreateFnAgent.mockImplementation(() => {
|
||||
createCount++;
|
||||
if (createCount <= 4) {
|
||||
// Step 0: 1 initial + 3 retries = 4 failures
|
||||
@@ -831,7 +831,7 @@ describe("StepSessionExecutor", () => {
|
||||
});
|
||||
|
||||
// 4 sessions for step 0 + 1 for step 1
|
||||
expect(mockedCreateKbAgent).toHaveBeenCalledTimes(5);
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("aborted flag: returns failed result immediately", async () => {
|
||||
@@ -843,7 +843,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 1 });
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
taskDetail: task,
|
||||
@@ -889,7 +889,7 @@ describe("StepSessionExecutor", () => {
|
||||
const semaphore = new AgentSemaphore(2);
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
|
||||
const acquireSpy = vi.spyOn(semaphore, "acquire");
|
||||
const releaseSpy = vi.spyOn(semaphore, "release");
|
||||
@@ -917,7 +917,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 1 });
|
||||
const semaphore = new AgentSemaphore(1);
|
||||
|
||||
mockedCreateKbAgent.mockRejectedValue(new Error("Agent creation failed"));
|
||||
mockedCreateFnAgent.mockRejectedValue(new Error("Agent creation failed"));
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
taskDetail: task,
|
||||
@@ -956,7 +956,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 2 });
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
mockedExecSync.mockReturnValue("");
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
@@ -998,7 +998,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 2 });
|
||||
|
||||
let createCount = 0;
|
||||
mockedCreateKbAgent.mockImplementation(() => {
|
||||
mockedCreateFnAgent.mockImplementation(() => {
|
||||
createCount++;
|
||||
if (createCount === 1) {
|
||||
// Step 0 succeeds
|
||||
@@ -1055,7 +1055,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 2 });
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
|
||||
// Make git log return commits, but cherry-pick fails
|
||||
mockedExecSync.mockImplementation((cmd: string) => {
|
||||
@@ -1101,7 +1101,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 2 });
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
|
||||
mockedExecSync.mockImplementation((cmd: string) => {
|
||||
if (typeof cmd === "string" && cmd.includes("git log")) {
|
||||
@@ -1183,7 +1183,7 @@ describe("StepSessionExecutor", () => {
|
||||
const semaphore = new AgentSemaphore(4);
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
mockedExecSync.mockReturnValue("");
|
||||
|
||||
const acquireSpy = vi.spyOn(semaphore, "acquire");
|
||||
@@ -1245,7 +1245,7 @@ describe("StepSessionExecutor", () => {
|
||||
});
|
||||
const executionEvents: string[] = [];
|
||||
|
||||
mockedCreateKbAgent.mockImplementation(({ cwd }: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(({ cwd }: any) => {
|
||||
if (cwd === "/project/.worktrees/wt-step-0") {
|
||||
return Promise.resolve({
|
||||
session: makeMockSession(async () => {
|
||||
@@ -1288,7 +1288,7 @@ describe("StepSessionExecutor", () => {
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.map((r) => r.stepIndex)).toEqual([0, 1]);
|
||||
expect(results.every((r) => r.success)).toBe(true);
|
||||
expect(mockedCreateKbAgent).toHaveBeenNthCalledWith(
|
||||
expect(mockedCreateFnAgent).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ cwd: "/project/.worktrees/main" }),
|
||||
);
|
||||
@@ -1330,7 +1330,7 @@ describe("StepSessionExecutor", () => {
|
||||
let maxActivePrimarySteps = 0;
|
||||
const cwdOrder: string[] = [];
|
||||
|
||||
mockedCreateKbAgent.mockImplementation(({ cwd }: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(({ cwd }: any) => {
|
||||
return Promise.resolve({
|
||||
session: makeMockSession(async () => {
|
||||
cwdOrder.push(cwd);
|
||||
@@ -1412,7 +1412,7 @@ describe("StepSessionExecutor", () => {
|
||||
let maxActiveParallelSteps = 0;
|
||||
const events: string[] = [];
|
||||
|
||||
mockedCreateKbAgent.mockImplementation(({ cwd }: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(({ cwd }: any) => {
|
||||
if (cwd === "/project/.worktrees/main") {
|
||||
return Promise.resolve({
|
||||
session: makeMockSession(async () => {
|
||||
@@ -1461,7 +1461,7 @@ describe("StepSessionExecutor", () => {
|
||||
expect(results.every((r) => r.success)).toBe(true);
|
||||
expect(maxActiveParallelSteps).toBe(2);
|
||||
|
||||
const primaryCwdCalls = mockedCreateKbAgent.mock.calls.filter(
|
||||
const primaryCwdCalls = mockedCreateFnAgent.mock.calls.filter(
|
||||
([opts]) => (opts as { cwd?: string }).cwd === "/project/.worktrees/main",
|
||||
);
|
||||
expect(primaryCwdCalls).toHaveLength(1);
|
||||
@@ -1498,7 +1498,7 @@ describe("StepSessionExecutor", () => {
|
||||
.mockImplementationOnce(() => "wt-success-0")
|
||||
.mockImplementationOnce(() => "wt-success-1");
|
||||
mockedExecSync.mockReturnValue("");
|
||||
mockedCreateKbAgent.mockImplementation(({ cwd }: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(({ cwd }: any) => {
|
||||
return Promise.resolve({
|
||||
session: makeMockSession(async () => {
|
||||
expect(cwd).not.toBe("/project/.worktrees/main");
|
||||
@@ -1519,7 +1519,7 @@ describe("StepSessionExecutor", () => {
|
||||
expect(results.map((r) => r.stepIndex)).toEqual([0, 1]);
|
||||
expect(results.every((r) => r.success)).toBe(true);
|
||||
|
||||
const primaryCwdCalls = mockedCreateKbAgent.mock.calls.filter(
|
||||
const primaryCwdCalls = mockedCreateFnAgent.mock.calls.filter(
|
||||
([opts]) => (opts as { cwd?: string }).cwd === "/project/.worktrees/main",
|
||||
);
|
||||
expect(primaryCwdCalls).toHaveLength(0);
|
||||
@@ -1563,7 +1563,7 @@ describe("StepSessionExecutor", () => {
|
||||
return "";
|
||||
});
|
||||
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: makeMockSession() } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: makeMockSession() } as any);
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
taskDetail: task,
|
||||
@@ -1602,7 +1602,7 @@ describe("StepSessionExecutor", () => {
|
||||
throw new Error("dispose failed");
|
||||
});
|
||||
|
||||
mockedCreateKbAgent.mockResolvedValue({ session: failingSession } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: failingSession } as any);
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
taskDetail: task,
|
||||
@@ -1629,7 +1629,7 @@ describe("StepSessionExecutor", () => {
|
||||
const session1 = makeMockSession();
|
||||
|
||||
let createCount = 0;
|
||||
mockedCreateKbAgent.mockImplementation(() => {
|
||||
mockedCreateFnAgent.mockImplementation(() => {
|
||||
createCount++;
|
||||
const s = createCount === 1 ? session0 : session1;
|
||||
return Promise.resolve({ session: s } as any);
|
||||
@@ -1697,7 +1697,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 2 });
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
mockedExecSync.mockReturnValue("");
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
@@ -1759,7 +1759,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 2 });
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
mockedExecSync.mockReturnValue("");
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
@@ -1788,7 +1788,7 @@ describe("StepSessionExecutor", () => {
|
||||
const settings = makeSettings({ maxParallelSteps: 1 });
|
||||
|
||||
const session = makeMockSession();
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
taskDetail: task,
|
||||
@@ -1825,7 +1825,7 @@ describe("StepSessionExecutor", () => {
|
||||
onToolEnd?.("read", false, "ok");
|
||||
});
|
||||
|
||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
onText = opts.onText;
|
||||
onToolStart = opts.onToolStart;
|
||||
onToolEnd = opts.onToolEnd;
|
||||
@@ -1859,7 +1859,7 @@ describe("StepSessionExecutor", () => {
|
||||
const session = makeMockSession(async () => {
|
||||
throw new Error("step failed");
|
||||
});
|
||||
mockedCreateKbAgent.mockResolvedValue({ session } as any);
|
||||
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||
|
||||
const executor = new StepSessionExecutor({
|
||||
store,
|
||||
@@ -1897,7 +1897,7 @@ describe("StepSessionExecutor", () => {
|
||||
const store = { appendAgentLog } as unknown as TaskStore;
|
||||
|
||||
// Create session that throws context-limit error on first prompt
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: makeMockSession(),
|
||||
} as any);
|
||||
|
||||
@@ -1943,7 +1943,7 @@ describe("StepSessionExecutor", () => {
|
||||
const appendAgentLog = vi.fn().mockResolvedValue(undefined);
|
||||
const store = { appendAgentLog } as unknown as TaskStore;
|
||||
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: makeMockSession(),
|
||||
} as any);
|
||||
|
||||
@@ -1985,7 +1985,7 @@ describe("StepSessionExecutor", () => {
|
||||
const appendAgentLog = vi.fn().mockResolvedValue(undefined);
|
||||
const store = { appendAgentLog } as unknown as TaskStore;
|
||||
|
||||
mockedCreateKbAgent.mockResolvedValue({
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: makeMockSession(),
|
||||
} as any);
|
||||
|
||||
@@ -2085,7 +2085,7 @@ describe("StepSessionExecutor tool availability", () => {
|
||||
let captured: any[] = [];
|
||||
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
captured = opts.customTools || [];
|
||||
return {
|
||||
session: {
|
||||
|
||||
@@ -13,9 +13,9 @@ import { tmpdir } from "node:os";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { triageLog } from "./logger.js";
|
||||
|
||||
const { mockReviewStep, mockCreateKbAgent } = vi.hoisted(() => ({
|
||||
const { mockReviewStep, mockCreateFnAgent } = vi.hoisted(() => ({
|
||||
mockReviewStep: vi.fn(),
|
||||
mockCreateKbAgent: vi.fn(),
|
||||
mockCreateFnAgent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./reviewer.js", () => ({
|
||||
@@ -23,7 +23,7 @@ vi.mock("./reviewer.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./pi.js", () => ({
|
||||
createFnAgent: mockCreateKbAgent,
|
||||
createFnAgent: mockCreateFnAgent,
|
||||
describeModel: vi.fn().mockReturnValue("mock-model"),
|
||||
promptWithFallback: vi.fn().mockReturnValue("mock-prompt"),
|
||||
}));
|
||||
@@ -1332,7 +1332,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
// Capture customTools from createFnAgent call
|
||||
let capturedCustomTools: any[] = [];
|
||||
const mockDispose = vi.fn();
|
||||
mockCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedCustomTools = opts.customTools || [];
|
||||
return {
|
||||
session: {
|
||||
@@ -1404,7 +1404,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
getTask: vi.fn().mockResolvedValue({ ...task, attachments: [], comments: [] }),
|
||||
});
|
||||
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
@@ -1458,7 +1458,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
});
|
||||
|
||||
// Mock createFnAgent to throw a transient error
|
||||
mockCreateKbAgent.mockRejectedValue(new Error("upstream connect error"));
|
||||
mockCreateFnAgent.mockRejectedValue(new Error("upstream connect error"));
|
||||
|
||||
await processor.specifyTask(task);
|
||||
|
||||
@@ -1492,7 +1492,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
onSpecifyError,
|
||||
});
|
||||
|
||||
mockCreateKbAgent.mockRejectedValue(new Error("connection reset"));
|
||||
mockCreateFnAgent.mockRejectedValue(new Error("connection reset"));
|
||||
|
||||
await processor.specifyTask(task);
|
||||
|
||||
@@ -1637,7 +1637,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
// after the model log line, so we can verify the appendAgentLog call.
|
||||
// The session will be created, model logged, then promptWithFallback
|
||||
// throws — but the model log has already been written.
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: mockPrompt,
|
||||
dispose: mockDispose,
|
||||
@@ -1707,7 +1707,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
} as Settings),
|
||||
});
|
||||
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: mockPrompt,
|
||||
dispose: mockDispose,
|
||||
@@ -1730,7 +1730,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
await processor.specifyTask(task);
|
||||
|
||||
// Per-task override should take precedence over settings
|
||||
expect(mockCreateKbAgent).toHaveBeenCalledWith(
|
||||
expect(mockCreateFnAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultProvider: "google",
|
||||
defaultModelId: "gemini-2.5-pro",
|
||||
@@ -1772,7 +1772,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
} as Settings),
|
||||
});
|
||||
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: mockPrompt,
|
||||
dispose: mockDispose,
|
||||
@@ -1795,7 +1795,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
await processor.specifyTask(task);
|
||||
|
||||
// Should use settings planning model when no per-task override
|
||||
expect(mockCreateKbAgent).toHaveBeenCalledWith(
|
||||
expect(mockCreateFnAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
@@ -1835,7 +1835,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
} as Settings),
|
||||
});
|
||||
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: mockPrompt,
|
||||
dispose: mockDispose,
|
||||
@@ -1858,7 +1858,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
await processor.specifyTask(task);
|
||||
|
||||
// Should fall back to global defaults
|
||||
expect(mockCreateKbAgent).toHaveBeenCalledWith(
|
||||
expect(mockCreateFnAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultProvider: "anthropic",
|
||||
defaultModelId: "claude-sonnet-4-5",
|
||||
@@ -2117,7 +2117,7 @@ describe("pause-abort status clearing (bug fix)", () => {
|
||||
|
||||
let resolveDispose: () => void;
|
||||
const disposePromise = new Promise<void>((r) => { resolveDispose = r; });
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
state: {},
|
||||
sessionManager: {},
|
||||
@@ -2169,7 +2169,7 @@ describe("stuck task detector integration", () => {
|
||||
let mockDispose: ReturnType<typeof vi.fn>;
|
||||
const disposePromise = new Promise<void>((r) => { resolveDispose = r; });
|
||||
mockDispose = vi.fn().mockImplementation(() => resolveDispose());
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
state: {},
|
||||
sessionManager: {},
|
||||
@@ -2216,7 +2216,7 @@ describe("stuck task detector integration", () => {
|
||||
parseDependenciesFromPrompt: vi.fn().mockResolvedValue([]),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
state: {},
|
||||
sessionManager: {},
|
||||
@@ -2260,7 +2260,7 @@ describe("specifyTask — status restore failure diagnostics", () => {
|
||||
|
||||
let resolveDispose!: () => void;
|
||||
const disposePromise = new Promise<void>((r) => { resolveDispose = r; });
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
state: {},
|
||||
sessionManager: {},
|
||||
@@ -2312,7 +2312,7 @@ describe("specifyTask — status restore failure diagnostics", () => {
|
||||
let resolveDispose!: () => void;
|
||||
const disposePromise = new Promise<void>((r) => { resolveDispose = r; });
|
||||
const mockDispose = vi.fn().mockImplementation(() => resolveDispose());
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
state: {},
|
||||
sessionManager: {},
|
||||
@@ -2368,7 +2368,7 @@ describe("specifyTask — status restore failure diagnostics", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
state: {},
|
||||
sessionManager: { getLeafId: vi.fn().mockReturnValue(null) },
|
||||
@@ -2423,7 +2423,7 @@ describe("specifyTask — status restore failure diagnostics", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
mockCreateKbAgent.mockRejectedValueOnce(new Error("upstream connect error"));
|
||||
mockCreateFnAgent.mockRejectedValueOnce(new Error("upstream connect error"));
|
||||
|
||||
const processor = new TriageProcessor(store, "/test/root", {
|
||||
pollIntervalMs: 100_000,
|
||||
@@ -2449,7 +2449,7 @@ describe("tool callback behavior (FN-1500)", () => {
|
||||
// Access the agentLogger via internal agentWork closure
|
||||
// by running specifyTask and intercepting the createFnAgent call
|
||||
let capturedOnAgentTool: ((id: string, name: string) => void) | undefined;
|
||||
mockCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
// Capture the onToolStart callback that was passed to createFnAgent
|
||||
// This is the onAgentTool from agentLogger
|
||||
if (opts.onToolStart) {
|
||||
@@ -2487,7 +2487,7 @@ describe("tool callback behavior (FN-1500)", () => {
|
||||
const processor = new TriageProcessor(store, "/tmp/root");
|
||||
|
||||
let capturedOnToolStart: ((name: string, args?: Record<string, unknown>) => void) | undefined;
|
||||
mockCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
if (opts.onToolStart) {
|
||||
capturedOnToolStart = opts.onToolStart;
|
||||
}
|
||||
@@ -2528,7 +2528,7 @@ describe("tool callback behavior (FN-1500)", () => {
|
||||
const processor = new TriageProcessor(store, "/tmp/root");
|
||||
|
||||
let capturedOnToolStart: ((name: string, args?: Record<string, unknown>) => void) | undefined;
|
||||
mockCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||
mockCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
if (opts.onToolStart) {
|
||||
capturedOnToolStart = opts.onToolStart;
|
||||
}
|
||||
@@ -2574,7 +2574,7 @@ describe("TriageProcessor skillSelection regression (FN-1511)", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockCreateKbAgent.mockResolvedValue({
|
||||
mockCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
state: {},
|
||||
sessionManager: {},
|
||||
@@ -2588,7 +2588,7 @@ describe("TriageProcessor skillSelection regression (FN-1511)", () => {
|
||||
/**
|
||||
* Helper: execute triage on a task and capture createFnAgent call arguments.
|
||||
*/
|
||||
async function captureCreateKbAgentArgs(options?: {
|
||||
async function captureCreateFnAgentArgs(options?: {
|
||||
assignedAgentId?: string;
|
||||
assignedAgentSkills?: string[];
|
||||
}) {
|
||||
@@ -2626,7 +2626,7 @@ describe("TriageProcessor skillSelection regression (FN-1511)", () => {
|
||||
});
|
||||
|
||||
let capturedArgs: any = null;
|
||||
mockCreateKbAgent.mockImplementationOnce(async (opts: any) => {
|
||||
mockCreateFnAgent.mockImplementationOnce(async (opts: any) => {
|
||||
capturedArgs = opts;
|
||||
return {
|
||||
session: {
|
||||
@@ -2662,7 +2662,7 @@ describe("TriageProcessor skillSelection regression (FN-1511)", () => {
|
||||
|
||||
describe("skillSelection context propagation", () => {
|
||||
it("passes skillSelection to createFnAgent with correct projectRootDir", async () => {
|
||||
const args = await captureCreateKbAgentArgs({
|
||||
const args = await captureCreateFnAgentArgs({
|
||||
assignedAgentId: "agent-001",
|
||||
assignedAgentSkills: ["triage"],
|
||||
});
|
||||
@@ -2673,7 +2673,7 @@ describe("TriageProcessor skillSelection regression (FN-1511)", () => {
|
||||
});
|
||||
|
||||
it("uses 'triage' as sessionPurpose for triage sessions", async () => {
|
||||
const args = await captureCreateKbAgentArgs({
|
||||
const args = await captureCreateFnAgentArgs({
|
||||
assignedAgentId: "agent-001",
|
||||
assignedAgentSkills: ["triage"],
|
||||
});
|
||||
@@ -2700,7 +2700,7 @@ describe("TriageProcessor skillSelection regression (FN-1511)", () => {
|
||||
});
|
||||
|
||||
let capturedArgs: any = null;
|
||||
mockCreateKbAgent.mockImplementationOnce(async (opts: any) => {
|
||||
mockCreateFnAgent.mockImplementationOnce(async (opts: any) => {
|
||||
capturedArgs = opts;
|
||||
return {
|
||||
session: {
|
||||
@@ -2734,7 +2734,7 @@ describe("TriageProcessor skillSelection regression (FN-1511)", () => {
|
||||
|
||||
describe("parity with executor paths", () => {
|
||||
it("uses same skillSelection field structure as executor", async () => {
|
||||
const args = await captureCreateKbAgentArgs({
|
||||
const args = await captureCreateFnAgentArgs({
|
||||
assignedAgentId: "agent-001",
|
||||
assignedAgentSkills: ["triage"],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user