feat(FN-4146): append verification subtask to planning flow
FN-4146 adds workspace verification to the planning system, including a new verification subtask in the planning module and reconciling the corresponding route tests. The bulk of the work is in `planning.ts` and its test files, with a minor adjustment to the merger overlap guard test. Fusion-Task-Id: FN-4146
This commit is contained in:
@@ -2053,7 +2053,7 @@ describe("planning module", () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("generates subtasks from keyDeliverables", async () => {
|
||||
it("generates subtasks from keyDeliverables and appends verification", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const sessionId = await createCompletedSession(mockIp, "Build auth system");
|
||||
|
||||
@@ -2061,7 +2061,7 @@ describe("planning module", () => {
|
||||
|
||||
// The AI-generated session produces 3 key deliverables:
|
||||
// "Implementation", "Tests", "Documentation"
|
||||
expect(result.length).toBe(3);
|
||||
expect(result.length).toBe(4);
|
||||
|
||||
// First subtask has no dependencies
|
||||
expect(result[0]).toEqual({
|
||||
@@ -2083,7 +2083,7 @@ describe("planning module", () => {
|
||||
dependsOn: ["subtask-1"],
|
||||
});
|
||||
|
||||
// Third subtask depends on second
|
||||
// Third deliverable subtask depends on second
|
||||
expect(result[2]).toEqual({
|
||||
id: "subtask-3",
|
||||
title: "Documentation",
|
||||
@@ -2092,6 +2092,16 @@ describe("planning module", () => {
|
||||
priority: "normal",
|
||||
dependsOn: ["subtask-2"],
|
||||
});
|
||||
|
||||
expect(result[3]).toEqual({
|
||||
id: "subtask-4",
|
||||
title: "Verify end-to-end",
|
||||
description: expect.any(String),
|
||||
suggestedSize: "S",
|
||||
priority: "normal",
|
||||
dependsOn: ["subtask-3"],
|
||||
});
|
||||
expect(result[3]?.description).toContain("Verify the full plan end-to-end now that all deliverables are implemented.");
|
||||
});
|
||||
|
||||
it("inherits summary priority for generated subtasks", async () => {
|
||||
@@ -2115,10 +2125,11 @@ describe("planning module", () => {
|
||||
|
||||
const result = generateSubtasksFromPlanning(sessionId);
|
||||
|
||||
expect(result.length).toBe(3);
|
||||
expect(result.length).toBe(4);
|
||||
expect(result[0]?.description).toContain('Implement "Implementation" as this subtask\'s primary outcome.');
|
||||
expect(result[1]?.description).toContain('Implement "Tests" as this subtask\'s primary outcome.');
|
||||
expect(result[2]?.description).toContain('Implement "Documentation" as this subtask\'s primary outcome.');
|
||||
expect(result[3]?.description).toContain("Verify the full plan end-to-end now that all deliverables are implemented.");
|
||||
|
||||
expect(result[0]?.description).toContain("## Larger Plan Context");
|
||||
expect(result[0]?.description).toContain("## Planning Interview Context");
|
||||
@@ -2200,7 +2211,7 @@ describe("planning module", () => {
|
||||
expect(result[0]?.description).toContain("## Larger Plan Context");
|
||||
});
|
||||
|
||||
it("assigns correct sizes based on deliverable position", async () => {
|
||||
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);
|
||||
|
||||
@@ -2222,14 +2233,16 @@ describe("planning module", () => {
|
||||
}
|
||||
|
||||
const result = generateSubtasksFromPlanning(sessionId);
|
||||
expect(result.length).toBe(5);
|
||||
expect(result.length).toBe(6);
|
||||
|
||||
// First: S, Middle: M, Last: S
|
||||
// First: S, Middle: M, Last deliverable: S, Verification: S
|
||||
expect(result[0]?.suggestedSize).toBe("S");
|
||||
expect(result[1]?.suggestedSize).toBe("M");
|
||||
expect(result[2]?.suggestedSize).toBe("M");
|
||||
expect(result[3]?.suggestedSize).toBe("M");
|
||||
expect(result[4]?.suggestedSize).toBe("S");
|
||||
expect(result[5]?.title).toBe("Verify end-to-end");
|
||||
expect(result[5]?.suggestedSize).toBe("S");
|
||||
});
|
||||
|
||||
it("uses sequential dependencies between subtasks", async () => {
|
||||
@@ -2244,11 +2257,43 @@ 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);
|
||||
|
||||
await submitResponse(sessionId, { scope: "small" });
|
||||
await submitResponse(sessionId, { requirements: "one thing" });
|
||||
await submitResponse(sessionId, { confirm: true });
|
||||
|
||||
const session = getSession(sessionId);
|
||||
if (session?.summary) {
|
||||
session.summary.keyDeliverables = ["Only one"];
|
||||
}
|
||||
|
||||
const result = generateSubtasksFromPlanning(sessionId);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]?.id).toBe("subtask-1");
|
||||
expect(result[0]?.title).toBe("Only one");
|
||||
expect(result[1]).toEqual(expect.objectContaining({
|
||||
id: "subtask-2",
|
||||
title: "Verify end-to-end",
|
||||
suggestedSize: "S",
|
||||
dependsOn: ["subtask-1"],
|
||||
}));
|
||||
});
|
||||
|
||||
it("merges compact subtask drafts onto generated planning subtasks", async () => {
|
||||
const mockIp = getUniqueIp();
|
||||
const sessionId = await createCompletedSession(mockIp, "Compact draft merge test");
|
||||
|
||||
const generated = generateSubtasksFromPlanning(sessionId);
|
||||
const verificationSubtask = generated.at(-1);
|
||||
expect(verificationSubtask).toEqual(expect.objectContaining({
|
||||
id: "subtask-4",
|
||||
title: "Verify end-to-end",
|
||||
dependsOn: ["subtask-3"],
|
||||
}));
|
||||
|
||||
const merged = mergePlanningSubtaskDrafts(sessionId, [
|
||||
{ id: generated[0]!.id },
|
||||
{
|
||||
@@ -2263,6 +2308,12 @@ describe("planning module", () => {
|
||||
id: generated[2]!.id,
|
||||
dependsOn: [generated[0]!.id, generated[1]!.id],
|
||||
},
|
||||
{
|
||||
id: verificationSubtask!.id,
|
||||
title: "Edited verification",
|
||||
description: "Run end-to-end verification and capture follow-ups",
|
||||
dependsOn: [generated[1]!.id, generated[2]!.id],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(merged[0]).toEqual(generated[0]);
|
||||
@@ -2277,6 +2328,12 @@ describe("planning module", () => {
|
||||
...generated[2],
|
||||
dependsOn: [generated[0]!.id, generated[1]!.id],
|
||||
});
|
||||
expect(merged[3]).toEqual({
|
||||
...verificationSubtask,
|
||||
title: "Edited verification",
|
||||
description: "Run end-to-end verification and capture follow-ups",
|
||||
dependsOn: [generated[1]!.id, generated[2]!.id],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves client-added subtasks when merging compact drafts", async () => {
|
||||
|
||||
@@ -1264,13 +1264,21 @@ describe("Planning Mode Routes", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.sessionId).toBe(sessionId);
|
||||
expect(res.body.subtasks).toHaveLength(1);
|
||||
expect(res.body.subtasks).toHaveLength(2);
|
||||
expect(res.body.subtasks[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
title: "OAuth integration",
|
||||
}),
|
||||
);
|
||||
expect(res.body.subtasks[0].description).toContain("Use OAuth providers and secure refresh tokens");
|
||||
expect(res.body.subtasks[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "subtask-2",
|
||||
title: "Verify end-to-end",
|
||||
dependsOn: ["subtask-1"],
|
||||
suggestedSize: "S",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1738,7 +1746,7 @@ describe("Planning Mode Routes", () => {
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
for (let index = 0; index < 15; index += 1) {
|
||||
for (let index = 0; index < 16; index += 1) {
|
||||
(store.createTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...createdTaskBase,
|
||||
id: `FN-${300 + index}`,
|
||||
@@ -1786,7 +1794,15 @@ describe("Planning Mode Routes", () => {
|
||||
priority?: string;
|
||||
dependsOn: string[];
|
||||
}>;
|
||||
expect(generatedSubtasks).toHaveLength(15);
|
||||
expect(generatedSubtasks).toHaveLength(16);
|
||||
expect(generatedSubtasks[15]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "subtask-16",
|
||||
title: "Verify end-to-end",
|
||||
dependsOn: ["subtask-15"],
|
||||
suggestedSize: "S",
|
||||
}),
|
||||
);
|
||||
|
||||
const oversizedLegacyPayload = JSON.stringify({ planningSessionId, subtasks: generatedSubtasks });
|
||||
const compactPayload = JSON.stringify({
|
||||
@@ -1805,7 +1821,7 @@ describe("Planning Mode Routes", () => {
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(store.createTask).toHaveBeenCalledTimes(15);
|
||||
expect(store.createTask).toHaveBeenCalledTimes(16);
|
||||
expect(store.createTask).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
@@ -1814,13 +1830,13 @@ describe("Planning Mode Routes", () => {
|
||||
}),
|
||||
);
|
||||
expect(store.createTask).toHaveBeenNthCalledWith(
|
||||
15,
|
||||
16,
|
||||
expect.objectContaining({
|
||||
title: generatedSubtasks[14]!.title,
|
||||
description: generatedSubtasks[14]!.description,
|
||||
title: generatedSubtasks[15]!.title,
|
||||
description: generatedSubtasks[15]!.description,
|
||||
}),
|
||||
);
|
||||
expect(store.logEntry).toHaveBeenCalledTimes(15);
|
||||
expect(store.logEntry).toHaveBeenCalledTimes(16);
|
||||
});
|
||||
|
||||
it("applies branchSelection when creating a planning task", async () => {
|
||||
|
||||
@@ -42,6 +42,17 @@ const { mockCreateFnAgent } = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("@fusion/engine", () => ({
|
||||
createFnAgent: mockCreateFnAgent,
|
||||
createResolvedAgentSession: vi.fn(async () => ({
|
||||
session: { state: { messages: [] }, prompt: vi.fn(), dispose: vi.fn() },
|
||||
runtimeModel: undefined,
|
||||
})),
|
||||
promptWithFallback: vi.fn(async (session: { prompt: (message: string) => Promise<void> }, prompt: string) => {
|
||||
await session.prompt(prompt);
|
||||
}),
|
||||
extractRuntimeHint: vi.fn(() => undefined),
|
||||
extractRuntimeModel: vi.fn(() => undefined),
|
||||
createSendMessageTool: vi.fn(() => ({})),
|
||||
createReadMessagesTool: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
function makePlanningAgent(responses: string[]) {
|
||||
|
||||
Reference in New Issue
Block a user