test(FN-4620): complete Step 6 — wire board tools assertions
Fusion-Task-Id: FN-4620 Fusion-Task-Lineage: d3d05e54-e137-46ee-bb5d-d71aa03e5a48
This commit is contained in:
@@ -38,6 +38,14 @@ import {
|
||||
type MilestoneInterviewSummary,
|
||||
type SliceInterviewSummary,
|
||||
} from "../milestone-slice-interview.js";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
const MOCK_TASK_STORE = {
|
||||
listTasks: vi.fn(async () => []),
|
||||
getTask: vi.fn(async () => {
|
||||
throw new Error("not found");
|
||||
}),
|
||||
} as unknown as TaskStore;
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { AiSessionRow } from "../ai-session-store.js";
|
||||
|
||||
@@ -233,12 +241,17 @@ describe("milestone-slice-interview module", () => {
|
||||
"ms-123",
|
||||
"Launch Platform",
|
||||
"Mission: Launch Platform v2",
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
|
||||
const session = getTargetInterviewSession(sessionId);
|
||||
expect(session).toBeDefined();
|
||||
expect(session?.targetType).toBe("milestone");
|
||||
const createFnAgentCallArg = mockCreateFnAgent.mock.calls.at(-1)?.[0] as { customTools?: Array<{ name: string }> };
|
||||
const customToolNames = createFnAgentCallArg.customTools?.map((tool) => tool.name) ?? [];
|
||||
expect(customToolNames).toContain("fn_task_list");
|
||||
expect(customToolNames).toContain("fn_task_get");
|
||||
expect(session?.targetId).toBe("ms-123");
|
||||
expect(session?.targetTitle).toBe("Launch Platform");
|
||||
expect(session?.missionContext).toBe("Mission: Launch Platform v2");
|
||||
@@ -254,7 +267,8 @@ describe("milestone-slice-interview module", () => {
|
||||
"sl-456",
|
||||
"Auth System",
|
||||
"Mission: Launch v2 | Milestone: Foundation",
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
|
||||
const session = getTargetInterviewSession(sessionId);
|
||||
@@ -275,7 +289,8 @@ describe("milestone-slice-interview module", () => {
|
||||
"ms-789",
|
||||
"Cancel mission",
|
||||
undefined,
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
@@ -402,7 +417,8 @@ describe("milestone-slice-interview module", () => {
|
||||
"ms-no-q",
|
||||
"Test",
|
||||
undefined,
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
@@ -426,7 +442,8 @@ describe("milestone-slice-interview module", () => {
|
||||
"ms-retry",
|
||||
"Retry Test",
|
||||
undefined,
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
|
||||
// Mark session as errored
|
||||
@@ -453,7 +470,8 @@ describe("milestone-slice-interview module", () => {
|
||||
"sl-no-error",
|
||||
"Not Error",
|
||||
undefined,
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
@@ -482,7 +500,8 @@ describe("milestone-slice-interview module", () => {
|
||||
"ms-apply",
|
||||
"Apply Test",
|
||||
"Mission context",
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
@@ -524,7 +543,8 @@ describe("milestone-slice-interview module", () => {
|
||||
"sl-apply",
|
||||
"Apply Slice Test",
|
||||
"Mission | Milestone context",
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
@@ -566,7 +586,8 @@ describe("milestone-slice-interview module", () => {
|
||||
"ms-cleanup",
|
||||
"Cleanup Test",
|
||||
"Context",
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
@@ -606,7 +627,8 @@ describe("milestone-slice-interview module", () => {
|
||||
"ms-no-summary",
|
||||
"No Summary",
|
||||
"Context",
|
||||
"/tmp/project"
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
@@ -794,6 +816,7 @@ describe("milestone-slice-interview module", () => {
|
||||
"Hung milestone interview",
|
||||
undefined,
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE,
|
||||
);
|
||||
|
||||
// Yield so the guard registration runs.
|
||||
@@ -831,6 +854,7 @@ describe("milestone-slice-interview module", () => {
|
||||
"Stoppable slice interview",
|
||||
undefined,
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE,
|
||||
);
|
||||
|
||||
let stopped = false;
|
||||
|
||||
@@ -37,8 +37,16 @@ import {
|
||||
} from "../ai-session-diagnostics.js";
|
||||
import type { LogEntry } from "../ai-session-diagnostics.js";
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import type { AiSessionRow } from "../ai-session-store.js";
|
||||
|
||||
const MOCK_TASK_STORE = {
|
||||
listTasks: vi.fn(async () => []),
|
||||
getTask: vi.fn(async () => {
|
||||
throw new Error("not found");
|
||||
}),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
function createQuestionJson(id = "q-1"): string {
|
||||
return JSON.stringify({
|
||||
type: "question",
|
||||
@@ -188,7 +196,7 @@ describe("mission-interview module", () => {
|
||||
|
||||
describe("session lifecycle", () => {
|
||||
it("creates, retrieves, and cleans up a session", async () => {
|
||||
const sessionId = await createMissionInterviewSession("127.0.0.1", "Launch platform", "/tmp/project");
|
||||
const sessionId = await createMissionInterviewSession("127.0.0.1", "Launch platform", "/tmp/project", MOCK_TASK_STORE);
|
||||
|
||||
const session = getMissionInterviewSession(sessionId);
|
||||
expect(session).toBeDefined();
|
||||
@@ -199,7 +207,7 @@ describe("mission-interview module", () => {
|
||||
});
|
||||
|
||||
it("cancels a session and throws when canceling missing session", async () => {
|
||||
const sessionId = await createMissionInterviewSession("127.0.0.2", "Cancel mission", "/tmp/project");
|
||||
const sessionId = await createMissionInterviewSession("127.0.0.2", "Cancel mission", "/tmp/project", MOCK_TASK_STORE);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
await cancelMissionInterviewSession(sessionId);
|
||||
@@ -214,10 +222,10 @@ describe("mission-interview module", () => {
|
||||
const ip = "10.0.0.1";
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await createMissionInterviewSession(ip, `Mission ${i}`, "/tmp/project");
|
||||
await createMissionInterviewSession(ip, `Mission ${i}`, "/tmp/project", MOCK_TASK_STORE);
|
||||
}
|
||||
|
||||
await expect(createMissionInterviewSession(ip, "Mission 6", "/tmp/project")).rejects.toBeInstanceOf(RateLimitError);
|
||||
await expect(createMissionInterviewSession(ip, "Mission 6", "/tmp/project", MOCK_TASK_STORE)).rejects.toBeInstanceOf(RateLimitError);
|
||||
expect(getRateLimitResetTime(ip)).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
@@ -339,7 +347,7 @@ describe("mission-interview module", () => {
|
||||
createMockAgent([createQuestionJson("q-plan"), createCompleteJson()]),
|
||||
);
|
||||
|
||||
const sessionId = await createMissionInterviewSession("172.16.0.1", "Build mission", "/tmp/project");
|
||||
const sessionId = await createMissionInterviewSession("172.16.0.1", "Build mission", "/tmp/project", MOCK_TASK_STORE);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
const session = getMissionInterviewSession(sessionId);
|
||||
@@ -404,7 +412,7 @@ describe("mission-interview module", () => {
|
||||
});
|
||||
|
||||
it("throws InvalidSessionStateError when no active question", async () => {
|
||||
const sessionId = await createMissionInterviewSession("172.16.0.2", "No question", "/tmp/project");
|
||||
const sessionId = await createMissionInterviewSession("172.16.0.2", "No question", "/tmp/project", MOCK_TASK_STORE);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
const session = getMissionInterviewSession(sessionId);
|
||||
@@ -452,7 +460,7 @@ describe("mission-interview module", () => {
|
||||
const resumedAgent = createMockAgent([createQuestionJson("q-retry")]);
|
||||
mockCreateFnAgent.mockImplementationOnce(async () => resumedAgent);
|
||||
|
||||
await retryMissionInterviewSession(row.id, "/tmp/project");
|
||||
await retryMissionInterviewSession(row.id, "/tmp/project", MOCK_TASK_STORE);
|
||||
|
||||
expect(resumedAgent.session.prompt).toHaveBeenCalledTimes(1);
|
||||
expect(resumedAgent.session.prompt.mock.calls[0]?.[0]).toContain("What is your goal?");
|
||||
@@ -483,7 +491,7 @@ describe("mission-interview module", () => {
|
||||
const resumedAgent = createMockAgent([createQuestionJson("q-first")]);
|
||||
mockCreateFnAgent.mockImplementationOnce(async () => resumedAgent);
|
||||
|
||||
await retryMissionInterviewSession(row.id, "/tmp/project");
|
||||
await retryMissionInterviewSession(row.id, "/tmp/project", MOCK_TASK_STORE);
|
||||
|
||||
expect(resumedAgent.session.prompt).toHaveBeenCalledTimes(1);
|
||||
expect(resumedAgent.session.prompt.mock.calls[0]?.[0]).toContain('I want to plan a mission: "Launch alpha"');
|
||||
@@ -706,9 +714,9 @@ describe("mission-interview module", () => {
|
||||
const mockAgent = createMockAgent([createQuestionJson()]);
|
||||
mockCreateFnAgent.mockImplementationOnce(async () => mockAgent);
|
||||
|
||||
await createMissionInterviewSession("192.168.1.1", "Test Mission", "/tmp/project");
|
||||
await createMissionInterviewSession("192.168.1.1", "Test Mission", "/tmp/project", MOCK_TASK_STORE);
|
||||
|
||||
await waitForCurrentQuestion(await createMissionInterviewSession("192.168.1.1", "Test Mission 2", "/tmp/project"));
|
||||
await waitForCurrentQuestion(await createMissionInterviewSession("192.168.1.1", "Test Mission 2", "/tmp/project", MOCK_TASK_STORE));
|
||||
|
||||
// The first session starts asynchronously, so we need to wait
|
||||
// Check the createFnAgent call was made with default prompt
|
||||
@@ -716,6 +724,9 @@ describe("mission-interview module", () => {
|
||||
const lastCall = mockCreateFnAgent.mock.calls[mockCreateFnAgent.mock.calls.length - 1];
|
||||
expect(lastCall[0].systemPrompt).toMatch(/^You are a mission planning assistant/);
|
||||
expect(lastCall[0].builtinToolsAllowlist).toEqual(["WebSearch", "WebFetch"]);
|
||||
const customToolNames = (lastCall[0].customTools as Array<{ name: string }> | undefined)?.map((tool) => tool.name) ?? [];
|
||||
expect(customToolNames).toContain("fn_task_list");
|
||||
expect(customToolNames).toContain("fn_task_get");
|
||||
});
|
||||
|
||||
it("uses override prompt when promptOverrides provided", async () => {
|
||||
@@ -726,6 +737,7 @@ describe("mission-interview module", () => {
|
||||
"192.168.1.2",
|
||||
"Test Mission",
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE,
|
||||
{ "mission-interview-system": customPrompt },
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
@@ -743,6 +755,7 @@ describe("mission-interview module", () => {
|
||||
"192.168.1.3",
|
||||
"Test Mission",
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE,
|
||||
{ "mission-interview-system": "" },
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
@@ -766,6 +779,7 @@ describe("mission-interview module", () => {
|
||||
row.id,
|
||||
{ "q-2": "Test response" },
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE,
|
||||
{ "mission-interview-system": customPrompt },
|
||||
);
|
||||
|
||||
@@ -798,6 +812,7 @@ describe("mission-interview module", () => {
|
||||
await retryMissionInterviewSession(
|
||||
row.id,
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE,
|
||||
{ "mission-interview-system": customPrompt },
|
||||
);
|
||||
|
||||
@@ -816,6 +831,7 @@ describe("mission-interview module", () => {
|
||||
"192.168.1.4",
|
||||
"Test Mission",
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE,
|
||||
{ "mission-interview-system": customPrompt },
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
@@ -857,6 +873,7 @@ describe("mission-interview module", () => {
|
||||
"192.168.1.5",
|
||||
"Test Mission",
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE,
|
||||
{ "planning-system": "Should not affect mission interview" },
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
@@ -886,7 +903,7 @@ describe("mission-interview module", () => {
|
||||
const resumedAgent = createMockAgent([createQuestionJson("q-retry")]);
|
||||
mockCreateFnAgent.mockImplementationOnce(async () => resumedAgent);
|
||||
|
||||
await retryMissionInterviewSession(row.id, "/tmp/project", undefined);
|
||||
await retryMissionInterviewSession(row.id, "/tmp/project", MOCK_TASK_STORE, undefined);
|
||||
|
||||
expect(mockCreateFnAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -905,7 +922,7 @@ describe("mission-interview module", () => {
|
||||
const resumedAgent = createMockAgent([createQuestionJson("q-fallback")]);
|
||||
mockCreateFnAgent.mockImplementationOnce(async () => resumedAgent);
|
||||
|
||||
await submitMissionInterviewResponse(row.id, { "q-2": "Test" }, "/tmp/project", undefined);
|
||||
await submitMissionInterviewResponse(row.id, { "q-2": "Test" }, "/tmp/project", MOCK_TASK_STORE, undefined);
|
||||
|
||||
expect(mockCreateFnAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -922,6 +939,7 @@ describe("mission-interview module", () => {
|
||||
"192.168.1.6",
|
||||
"Test Mission",
|
||||
"/tmp/project",
|
||||
MOCK_TASK_STORE,
|
||||
{},
|
||||
);
|
||||
await waitForCurrentQuestion(sessionId);
|
||||
|
||||
@@ -117,6 +117,13 @@ const STANDARD_QUESTION_RESPONSES = [
|
||||
/** Root dir for all test sessions */
|
||||
const TEST_ROOT_DIR = "/test/project";
|
||||
|
||||
const MOCK_TASK_STORE = {
|
||||
listTasks: vi.fn(async () => []),
|
||||
getTask: vi.fn(async () => {
|
||||
throw new Error("not found");
|
||||
}),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
// Counter for unique IPs per test
|
||||
let ipCounter = 0;
|
||||
function getUniqueIp(): string {
|
||||
@@ -300,7 +307,7 @@ describe("planning module", () => {
|
||||
describe("createSession", () => {
|
||||
it("creates a session with valid initial plan", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const result = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const result = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
expect(result.sessionId).toBeDefined();
|
||||
expect(typeof result.sessionId).toBe("string");
|
||||
@@ -318,11 +325,11 @@ describe("planning module", () => {
|
||||
const mockIp = getUniqueIp();
|
||||
// Create max sessions (1000 per hour)
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
await createSession(mockIp, `${initialPlan} ${i}`, undefined, TEST_ROOT_DIR);
|
||||
await createSession(mockIp, `${initialPlan} ${i}`, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
}
|
||||
|
||||
// 1001st session should fail
|
||||
await expect(createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR)).rejects.toThrow(RateLimitError);
|
||||
await expect(createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR)).rejects.toThrow(RateLimitError);
|
||||
});
|
||||
|
||||
it("allows new sessions after rate limit window expires", async () => {
|
||||
@@ -331,14 +338,14 @@ describe("planning module", () => {
|
||||
const mockIp = getUniqueIp();
|
||||
// Create max sessions
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
await createSession(mockIp, `${initialPlan} ${i}`, undefined, TEST_ROOT_DIR);
|
||||
await createSession(mockIp, `${initialPlan} ${i}`, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
}
|
||||
|
||||
// Advance time by 1 hour + 1 minute
|
||||
vi.advanceTimersByTime(61 * 60 * 1000);
|
||||
|
||||
// Should now be able to create a new session
|
||||
const result = await createSession(mockIp, "New plan after reset", undefined, TEST_ROOT_DIR);
|
||||
const result = await createSession(mockIp, "New plan after reset", MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
expect(result.sessionId).toBeDefined();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
@@ -347,15 +354,15 @@ describe("planning module", () => {
|
||||
|
||||
it("generates different session IDs for each session", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const result1 = await createSession(mockIp, "Plan 1", undefined, TEST_ROOT_DIR);
|
||||
const result2 = await createSession(mockIp, "Plan 2", undefined, TEST_ROOT_DIR);
|
||||
const result1 = await createSession(mockIp, "Plan 1", MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
const result2 = await createSession(mockIp, "Plan 2", MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
expect(result1.sessionId).not.toBe(result2.sessionId);
|
||||
});
|
||||
|
||||
it("stores the AI agent on the session", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
const session = getSession(sessionId);
|
||||
expect(session).toBeDefined();
|
||||
@@ -366,12 +373,16 @@ describe("planning module", () => {
|
||||
const createFnAgentSpy = vi.fn(async () => createMockAgent(STANDARD_QUESTION_RESPONSES));
|
||||
__setCreateFnAgent(createFnAgentSpy as any);
|
||||
|
||||
await createSession(getUniqueIp(), initialPlan, undefined, TEST_ROOT_DIR);
|
||||
await createSession(getUniqueIp(), initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
expect(createFnAgentSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
tools: "readonly",
|
||||
builtinToolsAllowlist: ["WebSearch", "WebFetch"],
|
||||
}));
|
||||
const callArg = createFnAgentSpy.mock.calls[0]?.[0] as { customTools?: Array<{ name: string }> };
|
||||
const customToolNames = callArg.customTools?.map((tool) => tool.name) ?? [];
|
||||
expect(customToolNames).toContain("fn_task_list");
|
||||
expect(customToolNames).toContain("fn_task_get");
|
||||
});
|
||||
|
||||
it("cleans up session on agent failure", async () => {
|
||||
@@ -380,7 +391,7 @@ describe("planning module", () => {
|
||||
});
|
||||
|
||||
const mockIp = getUniqueIp();
|
||||
await expect(createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR)).rejects.toThrow(
|
||||
await expect(createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR)).rejects.toThrow(
|
||||
"Agent creation failed"
|
||||
);
|
||||
});
|
||||
@@ -389,7 +400,7 @@ describe("planning module", () => {
|
||||
setupMockAgent(["I am not JSON at all"]);
|
||||
|
||||
const mockIp = getUniqueIp();
|
||||
await expect(createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR)).rejects.toThrow(
|
||||
await expect(createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR)).rejects.toThrow(
|
||||
"Failed to get first question from AI"
|
||||
);
|
||||
});
|
||||
@@ -409,7 +420,7 @@ describe("planning module", () => {
|
||||
]);
|
||||
|
||||
const mockIp = getUniqueIp();
|
||||
const result = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const result = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
// Should return a confirm question wrapping the summary
|
||||
expect(result.firstQuestion.type).toBe("confirm");
|
||||
@@ -427,6 +438,7 @@ describe("planning module", () => {
|
||||
getUniqueIp(),
|
||||
"Build auth system",
|
||||
TEST_ROOT_DIR,
|
||||
MOCK_TASK_STORE,
|
||||
"google",
|
||||
"gemini-2.5-pro",
|
||||
);
|
||||
@@ -449,7 +461,7 @@ describe("planning module", () => {
|
||||
const createFnAgentSpy = vi.fn(async () => createMockAgent(STANDARD_QUESTION_RESPONSES));
|
||||
__setCreateFnAgent(createFnAgentSpy as any);
|
||||
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), "Build auth system", TEST_ROOT_DIR);
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), "Build auth system", TEST_ROOT_DIR, MOCK_TASK_STORE);
|
||||
|
||||
expect(sessionId).toBeDefined();
|
||||
|
||||
@@ -461,6 +473,9 @@ describe("planning module", () => {
|
||||
expect(callArg?.defaultProvider).toBeUndefined();
|
||||
expect(callArg?.defaultModelId).toBeUndefined();
|
||||
expect(callArg?.builtinToolsAllowlist).toEqual(["WebSearch", "WebFetch"]);
|
||||
const customToolNames = (callArg?.customTools as Array<{ name: string }> | undefined)?.map((tool) => tool.name) ?? [];
|
||||
expect(customToolNames).toContain("fn_task_list");
|
||||
expect(customToolNames).toContain("fn_task_get");
|
||||
});
|
||||
|
||||
it("uses custom prompt from promptOverrides when provided", async () => {
|
||||
@@ -474,6 +489,7 @@ describe("planning module", () => {
|
||||
getUniqueIp(),
|
||||
"Build auth system",
|
||||
TEST_ROOT_DIR,
|
||||
MOCK_TASK_STORE,
|
||||
undefined,
|
||||
undefined,
|
||||
promptOverrides,
|
||||
@@ -497,6 +513,7 @@ describe("planning module", () => {
|
||||
getUniqueIp(),
|
||||
"Build auth system",
|
||||
TEST_ROOT_DIR,
|
||||
MOCK_TASK_STORE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -523,6 +540,7 @@ describe("planning module", () => {
|
||||
getUniqueIp(),
|
||||
"Build auth system",
|
||||
TEST_ROOT_DIR,
|
||||
MOCK_TASK_STORE,
|
||||
undefined,
|
||||
undefined,
|
||||
promptOverrides,
|
||||
@@ -605,6 +623,7 @@ describe("planning module", () => {
|
||||
getUniqueIp(),
|
||||
"Build auth system",
|
||||
TEST_ROOT_DIR,
|
||||
MOCK_TASK_STORE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -658,6 +677,7 @@ describe("planning module", () => {
|
||||
getUniqueIp(),
|
||||
"Build auth system",
|
||||
TEST_ROOT_DIR,
|
||||
MOCK_TASK_STORE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -716,6 +736,7 @@ describe("planning module", () => {
|
||||
getUniqueIp(),
|
||||
"Build auth system",
|
||||
TEST_ROOT_DIR,
|
||||
MOCK_TASK_STORE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -758,6 +779,7 @@ describe("planning module", () => {
|
||||
getUniqueIp(),
|
||||
"Build auth system",
|
||||
TEST_ROOT_DIR,
|
||||
MOCK_TASK_STORE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -798,7 +820,7 @@ describe("planning module", () => {
|
||||
TEST_ROOT_DIR,
|
||||
);
|
||||
|
||||
await startExistingSession(draft.sessionId, TEST_ROOT_DIR);
|
||||
await startExistingSession(draft.sessionId, TEST_ROOT_DIR, MOCK_TASK_STORE);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(getSession(draft.sessionId)?.currentQuestion?.id).toBe("q-scope");
|
||||
@@ -806,7 +828,7 @@ describe("planning module", () => {
|
||||
});
|
||||
|
||||
it("throws when starting a missing draft session", async () => {
|
||||
await expect(startExistingSession("missing-session", TEST_ROOT_DIR)).rejects.toThrow(
|
||||
await expect(startExistingSession("missing-session", TEST_ROOT_DIR, MOCK_TASK_STORE)).rejects.toThrow(
|
||||
SessionNotFoundError,
|
||||
);
|
||||
});
|
||||
@@ -815,7 +837,7 @@ describe("planning module", () => {
|
||||
describe("submitResponse", () => {
|
||||
it("processes response and returns next question", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
const response = await submitResponse(sessionId, { scope: "medium" });
|
||||
|
||||
@@ -827,7 +849,7 @@ describe("planning module", () => {
|
||||
|
||||
it("returns summary after multiple responses", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
// Submit first response
|
||||
const response1 = await submitResponse(sessionId, { scope: "medium" });
|
||||
@@ -855,7 +877,7 @@ describe("planning module", () => {
|
||||
|
||||
it("throws InvalidSessionStateError when no active question and not refining", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
// Complete the session
|
||||
await submitResponse(sessionId, { scope: "small" });
|
||||
@@ -881,7 +903,7 @@ describe("planning module", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
await submitResponse(sessionId, { scope: "small" }, TEST_ROOT_DIR);
|
||||
await submitResponse(sessionId, { requirements: "test" }, TEST_ROOT_DIR);
|
||||
await submitResponse(sessionId, { confirm: true }, TEST_ROOT_DIR);
|
||||
@@ -1030,7 +1052,7 @@ describe("planning module", () => {
|
||||
thinkingPerPrompt: ["First question reasoning"],
|
||||
});
|
||||
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR);
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR, MOCK_TASK_STORE);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(getSession(sessionId)?.currentQuestion?.id).toBe("q-scope");
|
||||
@@ -1045,7 +1067,7 @@ describe("planning module", () => {
|
||||
thinkingPerPrompt: ["First question thinking", "Second question thinking"],
|
||||
});
|
||||
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR);
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR, MOCK_TASK_STORE);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(getSession(sessionId)?.currentQuestion?.id).toBe("q-scope");
|
||||
@@ -1070,7 +1092,7 @@ describe("planning module", () => {
|
||||
thinkingPerPrompt: ["Persisted first-turn thinking", "Persisted second-turn thinking"],
|
||||
});
|
||||
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR);
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR, MOCK_TASK_STORE);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(getSession(sessionId)?.currentQuestion?.id).toBe("q-scope");
|
||||
@@ -1096,7 +1118,7 @@ describe("planning module", () => {
|
||||
|
||||
describe("rewindSession", () => {
|
||||
it("rewinds to the previous question and trims history", async () => {
|
||||
const { sessionId } = await createSession(getUniqueIp(), initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(getUniqueIp(), initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
await submitResponse(sessionId, { "q-scope": "medium" }, TEST_ROOT_DIR);
|
||||
|
||||
const rewound = await rewindSession(sessionId, TEST_ROOT_DIR);
|
||||
@@ -1109,7 +1131,7 @@ describe("planning module", () => {
|
||||
});
|
||||
|
||||
it("throws when no answered question exists", async () => {
|
||||
const { sessionId } = await createSession(getUniqueIp(), initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(getUniqueIp(), initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
await expect(rewindSession(sessionId, TEST_ROOT_DIR)).rejects.toThrow(InvalidSessionStateError);
|
||||
});
|
||||
});
|
||||
@@ -1303,7 +1325,7 @@ describe("planning module", () => {
|
||||
describe("cancelSession", () => {
|
||||
it("removes an active session", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
await cancelSession(sessionId);
|
||||
|
||||
@@ -1337,7 +1359,7 @@ describe("planning module", () => {
|
||||
};
|
||||
__setCreateFnAgent(async () => hangingAgent as any);
|
||||
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR);
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR, MOCK_TASK_STORE);
|
||||
await vi.waitFor(() => {
|
||||
expect(hangingAgent.session.prompt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -1364,7 +1386,7 @@ describe("planning module", () => {
|
||||
};
|
||||
__setCreateFnAgent(async () => hangingAgent as any);
|
||||
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR);
|
||||
const sessionId = await createSessionWithAgent(getUniqueIp(), initialPlan, TEST_ROOT_DIR, MOCK_TASK_STORE);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(GENERATION_TIMEOUT_MS + 10);
|
||||
await flushAsyncWork();
|
||||
@@ -1443,7 +1465,7 @@ describe("planning module", () => {
|
||||
describe("getSession", () => {
|
||||
it("returns session for valid ID", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
const session = getSession(sessionId);
|
||||
expect(session).toBeDefined();
|
||||
@@ -1456,7 +1478,7 @@ describe("planning module", () => {
|
||||
const store = new MockAiSessionStore();
|
||||
const getSpy = vi.spyOn(store, "get");
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
store.rows.set(
|
||||
sessionId,
|
||||
@@ -1500,7 +1522,7 @@ describe("planning module", () => {
|
||||
describe("getCurrentQuestion", () => {
|
||||
it("returns current question for active session", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId, firstQuestion } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId, firstQuestion } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
const question = getCurrentQuestion(sessionId);
|
||||
expect(question).toEqual(firstQuestion);
|
||||
@@ -1508,7 +1530,7 @@ describe("planning module", () => {
|
||||
|
||||
it("returns undefined for completed session", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
// Complete the session
|
||||
await submitResponse(sessionId, { scope: "small" });
|
||||
@@ -1523,7 +1545,7 @@ describe("planning module", () => {
|
||||
describe("getSummary", () => {
|
||||
it("returns summary for completed session", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
// Complete the session
|
||||
await submitResponse(sessionId, { scope: "small" });
|
||||
@@ -1538,7 +1560,7 @@ describe("planning module", () => {
|
||||
|
||||
it("returns undefined for incomplete session", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
const summary = getSummary(sessionId);
|
||||
expect(summary).toBeUndefined();
|
||||
@@ -1548,7 +1570,7 @@ describe("planning module", () => {
|
||||
describe("cleanupSession", () => {
|
||||
it("removes a session from memory", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
cleanupSession(sessionId);
|
||||
|
||||
@@ -1572,7 +1594,7 @@ describe("planning module", () => {
|
||||
|
||||
// Max out the rate limit
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await createSession(mockIp, `Plan ${i}`, undefined, TEST_ROOT_DIR);
|
||||
await createSession(mockIp, `Plan ${i}`, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
}
|
||||
|
||||
const resetTime = getRateLimitResetTime(mockIp);
|
||||
@@ -1590,7 +1612,7 @@ describe("planning module", () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
try {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, initialPlan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
// Advance beyond the old 30-minute TTL used prior to FN-1146.
|
||||
vi.advanceTimersByTime(31 * 60 * 1000);
|
||||
@@ -2032,7 +2054,7 @@ describe("planning module", () => {
|
||||
ip: string,
|
||||
plan: string
|
||||
): Promise<string> {
|
||||
const { sessionId } = await createSession(ip, plan, undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(ip, plan, MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
// Complete the session by submitting 3 responses
|
||||
await submitResponse(sessionId, { "q-scope": "medium" });
|
||||
await submitResponse(sessionId, { "q-requirements": "Test requirements" });
|
||||
@@ -2047,7 +2069,7 @@ describe("planning module", () => {
|
||||
|
||||
it("returns empty array if session has no summary (not complete)", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, "Incomplete session", undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, "Incomplete session", MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
const result = generateSubtasksFromPlanning(sessionId);
|
||||
expect(result).toEqual([]);
|
||||
@@ -2164,7 +2186,7 @@ describe("planning module", () => {
|
||||
|
||||
it("generates fallback subtasks when keyDeliverables is empty", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, "Fallback test", undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, "Fallback test", MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
// Complete the session normally, then manually clear keyDeliverables
|
||||
await submitResponse(sessionId, { scope: "small" });
|
||||
@@ -2213,7 +2235,7 @@ describe("planning module", () => {
|
||||
|
||||
it("assigns correct sizes based on deliverable position and appended verification", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, "Multi-deliverable test", undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, "Multi-deliverable test", MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
// Complete the session
|
||||
await submitResponse(sessionId, { scope: "large" });
|
||||
@@ -2259,7 +2281,7 @@ describe("planning module", () => {
|
||||
|
||||
it("appends verification after a single deliverable", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const { sessionId } = await createSession(mockIp, "Single deliverable test", undefined, TEST_ROOT_DIR);
|
||||
const { sessionId } = await createSession(mockIp, "Single deliverable test", MOCK_TASK_STORE, TEST_ROOT_DIR);
|
||||
|
||||
await submitResponse(sessionId, { scope: "small" });
|
||||
await submitResponse(sessionId, { requirements: "one thing" });
|
||||
@@ -3361,7 +3383,7 @@ describe("FN-3300: thinking-block response extraction", () => {
|
||||
const result = await createSession(
|
||||
getUniqueIp(),
|
||||
"Test plan",
|
||||
undefined,
|
||||
MOCK_TASK_STORE,
|
||||
TEST_ROOT_DIR,
|
||||
);
|
||||
|
||||
@@ -3391,7 +3413,7 @@ describe("FN-3300: thinking-block response extraction", () => {
|
||||
const result = await createSession(
|
||||
getUniqueIp(),
|
||||
"Test plan",
|
||||
undefined,
|
||||
MOCK_TASK_STORE,
|
||||
TEST_ROOT_DIR,
|
||||
);
|
||||
|
||||
@@ -3425,7 +3447,7 @@ describe("FN-3300: thinking-block response extraction", () => {
|
||||
__setCreateFnAgent(async () => agent);
|
||||
|
||||
await expect(
|
||||
createSession(getUniqueIp(), "Test plan", undefined, TEST_ROOT_DIR),
|
||||
createSession(getUniqueIp(), "Test plan", MOCK_TASK_STORE, TEST_ROOT_DIR),
|
||||
).rejects.toThrow("Failed to get first question from AI");
|
||||
|
||||
// Should have logged a warning about empty response text
|
||||
|
||||
@@ -1366,6 +1366,7 @@ async function ensureSessionAgent(
|
||||
rootDir: string | undefined,
|
||||
historyForReplay: Array<{ question: PlanningQuestion; response: unknown }>,
|
||||
promptOverrides?: PromptOverrideMap,
|
||||
store?: TaskStore,
|
||||
): Promise<void> {
|
||||
if (session.agent) {
|
||||
return;
|
||||
@@ -1377,7 +1378,13 @@ async function ensureSessionAgent(
|
||||
);
|
||||
}
|
||||
|
||||
session.agent = await createPlanningAgent(session, rootDir, undefined, undefined, promptOverrides);
|
||||
if (!store) {
|
||||
throw new InvalidSessionStateError(
|
||||
"Planning session has no task store and cannot be resumed without project context",
|
||||
);
|
||||
}
|
||||
|
||||
session.agent = await createPlanningAgent(session, rootDir, store, undefined, undefined, promptOverrides);
|
||||
|
||||
if (historyForReplay.length === 0) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user