FN-5799: preserve first-turn planning thinking output
Ensure first-question planning UI reliably shows streamed reasoning before question transitions. - Keep streamingOutputRef synchronized during onThinking updates so back-to-back thinking/question events preserve full reasoning text. - Expand PlanningModeModal question-flow tests to cover same-tick buffered replay and loading-view visibility for first-turn and follow-up thinking output. - Update assertions to verify reasoning content remains visible in conversation history after question handoff. Files changed: .../dashboard/app/components/PlanningModeModal.tsx | 9 +++- .../__tests__/PlanningModeModal.questions.test.tsx | 62 ++++++++++++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-5799 Fusion-Task-Lineage: 41677c47-4380-4145-a3d2-0e928fc40042
This commit is contained in:
@@ -474,7 +474,14 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
const connection = connectPlanningStream(sessionId, projectId, {
|
||||
onThinking: (data) => {
|
||||
if (isStaleEvent()) return;
|
||||
setStreamingOutput((prev) => prev + data);
|
||||
setStreamingOutput((prev) => {
|
||||
const next = prev + data;
|
||||
// Keep the ref synchronized inside the event handler so
|
||||
// back-to-back thinking/question events in the same flush can
|
||||
// still preserve the full reasoning payload.
|
||||
streamingOutputRef.current = next;
|
||||
return next;
|
||||
});
|
||||
broadcastUpdate({
|
||||
sessionId,
|
||||
status: "generating",
|
||||
|
||||
@@ -172,7 +172,7 @@ describe("PlanningModeModal", () => {
|
||||
});
|
||||
|
||||
describe("Initial-turn reasoning visibility (FN-3274)", () => {
|
||||
it("preserves reasoning in conversation history when first question arrives after thinking", async () => {
|
||||
it("shows first-turn thinking in loading view and preserves it when question follows immediately", async () => {
|
||||
let streamHandlers: any;
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
streamHandlers = handlers;
|
||||
@@ -202,17 +202,19 @@ describe("PlanningModeModal", () => {
|
||||
expect(screen.getByText("Generating next question...")).toBeDefined();
|
||||
});
|
||||
|
||||
// Simulate thinking output arriving during loading
|
||||
// Simulate buffered first-turn replay where thinking and question can
|
||||
// arrive back-to-back in the same flush.
|
||||
act(() => {
|
||||
streamHandlers.onThinking?.("Analyzing the plan requirements...");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("AI is thinking...")).toBeDefined();
|
||||
expect(screen.getByText("Analyzing the plan requirements...")).toBeDefined();
|
||||
});
|
||||
|
||||
// Transition to question view
|
||||
act(() => {
|
||||
streamHandlers.onThinking?.(" Buffered follow-up.");
|
||||
streamHandlers.onQuestion?.(mockQuestion);
|
||||
});
|
||||
|
||||
@@ -225,12 +227,58 @@ describe("PlanningModeModal", () => {
|
||||
expect(screen.getByTestId("conversation-history")).toBeDefined();
|
||||
expect(screen.getByText("AI Reasoning")).toBeDefined();
|
||||
fireEvent.click(screen.getByRole("button", { name: /Show AI reasoning/i }));
|
||||
expect(screen.getByText("Analyzing the plan requirements...")).toBeDefined();
|
||||
expect(screen.getByText("Analyzing the plan requirements... Buffered follow-up.")).toBeDefined();
|
||||
|
||||
// avoid dangling handlers reference lint
|
||||
expect(streamHandlers).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows first-turn thinking before question when replay arrives in same connect tick", async () => {
|
||||
vi.useFakeTimers();
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
handlers.onThinking?.("Synchronous buffered reasoning");
|
||||
handlers.onQuestion?.(mockQuestion);
|
||||
return {
|
||||
close: vi.fn(),
|
||||
isConnected: vi.fn().mockReturnValue(true),
|
||||
};
|
||||
});
|
||||
|
||||
render(
|
||||
<PlanningModeModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
onTaskCreated={mockOnTaskCreated}
|
||||
onTasksCreated={vi.fn()}
|
||||
tasks={mockTasks}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
|
||||
target: { value: "Build auth system" },
|
||||
});
|
||||
fireEvent.click(screen.getByText("Start Planning"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("AI is thinking...")).toBeDefined();
|
||||
expect(screen.getByText("Synchronous buffered reasoning")).toBeDefined();
|
||||
});
|
||||
expect(screen.queryByText("What is the scope?")).toBeNull();
|
||||
|
||||
act(() => {
|
||||
vi.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("What is the scope?")).toBeDefined();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Show AI reasoning/i }));
|
||||
expect(screen.getByText("Synchronous buffered reasoning")).toBeDefined();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("preserves reasoning in conversation history when summary arrives after thinking", async () => {
|
||||
let streamHandlers: any;
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
@@ -496,6 +544,12 @@ describe("PlanningModeModal", () => {
|
||||
fireEvent.click(screen.getByText("Medium"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
|
||||
// Second-turn parity: thinking should stream in loading view before the next question.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("AI is thinking...")).toBeDefined();
|
||||
expect(screen.getByText("Thinking about requirements...")).toBeDefined();
|
||||
});
|
||||
|
||||
// Wait for second question to arrive
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("What are the key requirements?")).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user