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:
Fusion
2026-05-15 11:40:33 -07:00
committed by gsxdsm
parent 808103b5ad
commit dd6cf58505
4 changed files with 138 additions and 67 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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;