fix(planning): wait for AI plan before review
Keep seeded fallback summaries out of SSE catch-up while generation is active so Refine and Validate cannot race the initial AI turn.
This commit is contained in:
7
.changeset/fix-planning-ai-start.md
Normal file
7
.changeset/fix-planning-ai-start.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Wait for the AI-authored Planning Mode plan before enabling review actions.
|
||||
category: fix
|
||||
dev: Suppresses seeded fallback summaries from SSE catch-up while a planning generation purpose remains active.
|
||||
@@ -790,6 +790,63 @@ describe("Planning Mode Routes", () => {
|
||||
});
|
||||
|
||||
describe("POST /planning/start-streaming", () => {
|
||||
it("does not expose the seeded fallback as a reviewable plan while the AI turn is active", async () => {
|
||||
const messages: Array<{ role: string; content: string }> = [];
|
||||
let releasePrompt: (() => void) | undefined;
|
||||
let markPromptStarted: (() => void) | undefined;
|
||||
const promptStarted = new Promise<void>((resolve) => {
|
||||
markPromptStarted = resolve;
|
||||
});
|
||||
__setCreateFnAgent(async () => ({
|
||||
session: {
|
||||
state: { messages },
|
||||
prompt: vi.fn(async (message: string) => {
|
||||
messages.push({ role: "user", content: message });
|
||||
markPromptStarted?.();
|
||||
await new Promise<void>((resolve) => {
|
||||
releasePrompt = resolve;
|
||||
});
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: JSON.stringify({
|
||||
type: "complete",
|
||||
data: {
|
||||
title: "AI-authored plan",
|
||||
description: "Generated after repository inspection.",
|
||||
proposedChanges: ["Implement the requested behavior"],
|
||||
acceptanceCriteria: ["The behavior is verified"],
|
||||
keyDeliverables: ["Working implementation"],
|
||||
},
|
||||
}),
|
||||
});
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const startRes = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/planning/start-streaming",
|
||||
JSON.stringify({ initialPlan: "Generate this plan with AI" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
const sessionId = startRes.body.sessionId as string;
|
||||
const streamPromise = REQUEST(buildApp(), "GET", `/api/planning/${sessionId}/stream`);
|
||||
|
||||
await promptStarted;
|
||||
planningStreamManager.broadcast(sessionId, { type: "complete" });
|
||||
const streamRes = await streamPromise;
|
||||
releasePrompt?.();
|
||||
await vi.waitFor(() => {
|
||||
expect(planningStreamManager.getBufferedEvents(sessionId, 0).some((event) => event.event === "summary")).toBe(true);
|
||||
});
|
||||
|
||||
expect(messages[0]?.content).toContain("Generate this plan with AI");
|
||||
expect(streamRes.body).not.toContain("event: summary");
|
||||
expect(streamRes.body).not.toContain("Generate this plan with AI");
|
||||
});
|
||||
|
||||
it("broadcasts a reviewable initial plan without an unsolicited question", async () => {
|
||||
const messages: Array<{ role: string; content: string }> = [];
|
||||
const responses = [
|
||||
|
||||
@@ -1723,7 +1723,15 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
awaiting-input question; only Validate writes `session.validated`, which authorizes a
|
||||
terminal complete event and closes the stream.
|
||||
*/
|
||||
if (session.summary) {
|
||||
/*
|
||||
FNXC:PlanningMode 2026-07-20-18:05:
|
||||
New and resumed sessions seed `summary` with deterministic fallback copy before the AI
|
||||
turn starts. While `generationPurpose` is set, that value is working state rather than a
|
||||
review-ready plan. Publishing it here moves the client out of loading early and exposes
|
||||
Refine/Validate against the still-active generation. Only catch up a settled summary;
|
||||
the generation path clears its purpose before broadcasting the AI-authored replacement.
|
||||
*/
|
||||
if (session.summary && session.generationPurpose === undefined) {
|
||||
const existing = planningStreamManager.getBufferedEvents(sessionId, 0);
|
||||
const lastSummaryEvent = [...existing].reverse().find((event) => event.event === "summary");
|
||||
const summaryEventId = lastSummaryEvent?.id
|
||||
|
||||
Reference in New Issue
Block a user