feat(FN-3274): preserve first-turn reasoning in planning modal
This merge introduces visibility of the AI's first-turn reasoning in the planning modal (FN-3274), with corresponding regression tests, and removes all PI (Prompt Intelligence) install references and documentation from the codebase (FN-3309). Fusion-Task-Id: FN-3274
This commit is contained in:
5
.changeset/fn-3274-planning-reasoning-visibility.md
Normal file
5
.changeset/fn-3274-planning-reasoning-visibility.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix planning mode reasoning visibility: AI thinking output is now preserved as expandable conversation history when transitioning from the loading state to the first question or summary, and when resuming persisted sessions.
|
||||||
@@ -19,8 +19,10 @@ Use the inline input on board/list view:
|
|||||||
Use the 💡 button to open planning mode:
|
Use the 💡 button to open planning mode:
|
||||||
|
|
||||||
- AI asks clarifying questions
|
- AI asks clarifying questions
|
||||||
|
- AI reasoning (thinking output) is preserved and visible throughout the session — expand the reasoning toggle to review the model's analysis before answering each question or accepting the summary
|
||||||
- Produces summary + key deliverables
|
- Produces summary + key deliverables
|
||||||
- Create one task or **Break into Tasks** (multi-task generation with dependencies)
|
- Create one task or **Break into Tasks** (multi-task generation with dependencies)
|
||||||
|
- Sessions persist when the modal is closed — resume from the sidebar list at any time; reasoning context is restored automatically
|
||||||
|
|
||||||
### 3) Todo item → Plan Mode
|
### 3) Todo item → Plan Mode
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
// target is the overlay and would dismiss the modal mid-resize.
|
// target is the overlay and would dismiss the modal mid-resize.
|
||||||
const overlayMouseDownOnSelfRef = useRef(false);
|
const overlayMouseDownOnSelfRef = useRef(false);
|
||||||
const thinkingOutputRef = useRef<HTMLDivElement>(null);
|
const thinkingOutputRef = useRef<HTMLDivElement>(null);
|
||||||
|
// Mirrors `streamingOutput` state for reading inside callbacks without
|
||||||
|
// stale closure issues (e.g. capturing reasoning before onQuestion clears it).
|
||||||
|
const streamingOutputRef = useRef<string>("" );
|
||||||
const draftSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const draftSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const lastSyncedDraftRef = useRef<{
|
const lastSyncedDraftRef = useRef<{
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -183,6 +186,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
useModalResizePersist(modalRef, isOpen, "fusion:planning-modal-size");
|
useModalResizePersist(modalRef, isOpen, "fusion:planning-modal-size");
|
||||||
const viewportMode = useViewportMode();
|
const viewportMode = useViewportMode();
|
||||||
|
|
||||||
|
// Mirror streamingOutput into a ref so SSE handlers can read the latest
|
||||||
|
// value without stale closure issues.
|
||||||
|
useEffect(() => {
|
||||||
|
streamingOutputRef.current = streamingOutput;
|
||||||
|
}, [streamingOutput]);
|
||||||
|
|
||||||
// Keep the streaming AI thinking pane pinned to the bottom as new tokens
|
// Keep the streaming AI thinking pane pinned to the bottom as new tokens
|
||||||
// arrive. If the user has scrolled up to read earlier output, we leave the
|
// arrive. If the user has scrolled up to read earlier output, we leave the
|
||||||
// scroll position alone — only auto-follow when they're already near the
|
// scroll position alone — only auto-follow when they're already near the
|
||||||
@@ -375,6 +384,23 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
setIsReconnecting(false);
|
setIsReconnecting(false);
|
||||||
setIsRetrying(false);
|
setIsRetrying(false);
|
||||||
clearPlanningDescription(projectId);
|
clearPlanningDescription(projectId);
|
||||||
|
|
||||||
|
// Preserve reasoning accumulated during the loading turn as a
|
||||||
|
// visible conversation-history entry so the user can expand it
|
||||||
|
// from the question view. Without this, setStreamingOutput("")
|
||||||
|
// would silently discard everything the model produced before the
|
||||||
|
// first question arrived.
|
||||||
|
const capturedThinking = streamingOutputRef.current.trim();
|
||||||
|
if (capturedThinking) {
|
||||||
|
setConversationHistory((prev) => {
|
||||||
|
// De-duplicate: if the last entry already carries this exact
|
||||||
|
// thinking text (e.g. from a prior transition or resume), skip.
|
||||||
|
const lastEntry = prev[prev.length - 1];
|
||||||
|
if (lastEntry?.thinkingOutput === capturedThinking) return prev;
|
||||||
|
return [...prev, { thinkingOutput: capturedThinking }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setView({
|
setView({
|
||||||
type: "question",
|
type: "question",
|
||||||
session: { sessionId, currentQuestion: question, summary: null },
|
session: { sessionId, currentQuestion: question, summary: null },
|
||||||
@@ -396,6 +422,17 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
setIsReconnecting(false);
|
setIsReconnecting(false);
|
||||||
setIsRetrying(false);
|
setIsRetrying(false);
|
||||||
clearPlanningDescription(projectId);
|
clearPlanningDescription(projectId);
|
||||||
|
|
||||||
|
// Preserve reasoning accumulated during the loading turn.
|
||||||
|
const capturedThinking = streamingOutputRef.current.trim();
|
||||||
|
if (capturedThinking) {
|
||||||
|
setConversationHistory((prev) => {
|
||||||
|
const lastEntry = prev[prev.length - 1];
|
||||||
|
if (lastEntry?.thinkingOutput === capturedThinking) return prev;
|
||||||
|
return [...prev, { thinkingOutput: capturedThinking }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setView({
|
setView({
|
||||||
type: "summary",
|
type: "summary",
|
||||||
session: { sessionId, currentQuestion: null, summary },
|
session: { sessionId, currentQuestion: null, summary },
|
||||||
@@ -661,7 +698,20 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
clearPlanningDescription(projectId);
|
clearPlanningDescription(projectId);
|
||||||
const question = JSON.parse(session.currentQuestion);
|
const question = JSON.parse(session.currentQuestion);
|
||||||
setView({ type: "question", session: { sessionId, currentQuestion: question, summary: null } });
|
setView({ type: "question", session: { sessionId, currentQuestion: question, summary: null } });
|
||||||
if (session.thinkingOutput) setStreamingOutput(session.thinkingOutput);
|
// Transfer persisted thinking into conversation history so it's
|
||||||
|
// visible as expandable reasoning in the question view, instead of
|
||||||
|
// setting streamingOutput which is only rendered in the loading
|
||||||
|
// state.
|
||||||
|
if (session.thinkingOutput) {
|
||||||
|
const trimmed = session.thinkingOutput.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
setConversationHistory((prev) => {
|
||||||
|
const lastEntry = prev[prev.length - 1];
|
||||||
|
if (lastEntry?.thinkingOutput === trimmed) return prev;
|
||||||
|
return [...prev, { thinkingOutput: trimmed }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
connectToPlanningStream(sessionId);
|
connectToPlanningStream(sessionId);
|
||||||
} else if (session.status === "complete" && session.result) {
|
} else if (session.status === "complete" && session.result) {
|
||||||
clearPlanningDescription(projectId);
|
clearPlanningDescription(projectId);
|
||||||
@@ -1174,13 +1224,25 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
// the frontend disconnects and reconnects after the API call.
|
// the frontend disconnects and reconnects after the API call.
|
||||||
|
|
||||||
setResponseHistory((prev) => [...prev, responses]);
|
setResponseHistory((prev) => [...prev, responses]);
|
||||||
setConversationHistory((prev) => [
|
setConversationHistory((prev) => {
|
||||||
...prev,
|
// Capture any reasoning that accumulated since the last question
|
||||||
|
// (e.g. thinking streamed while the user was reading the question).
|
||||||
|
const currentThinking = streamingOutputRef.current.trim();
|
||||||
|
let updated = prev;
|
||||||
|
if (currentThinking) {
|
||||||
|
const lastEntry = updated[updated.length - 1];
|
||||||
|
if (lastEntry?.thinkingOutput !== currentThinking) {
|
||||||
|
updated = [...updated, { thinkingOutput: currentThinking }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...updated,
|
||||||
{
|
{
|
||||||
question: activeQuestion,
|
question: activeQuestion,
|
||||||
response: responses,
|
response: responses,
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
});
|
||||||
setView({ type: "loading" });
|
setView({ type: "loading" });
|
||||||
setStreamingOutput(""); // Clear old thinking output when entering loading state
|
setStreamingOutput(""); // Clear old thinking output when entering loading state
|
||||||
|
|
||||||
@@ -1264,6 +1326,16 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
type: "question",
|
type: "question",
|
||||||
session: { sessionId: session.id, currentQuestion: question, summary: null },
|
session: { sessionId: session.id, currentQuestion: question, summary: null },
|
||||||
});
|
});
|
||||||
|
if (session.thinkingOutput) {
|
||||||
|
const trimmed = session.thinkingOutput.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
setConversationHistory((prev) => {
|
||||||
|
const lastEntry = prev[prev.length - 1];
|
||||||
|
if (lastEntry?.thinkingOutput === trimmed) return prev;
|
||||||
|
return [...prev, { thinkingOutput: trimmed }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!streamConnectionRef.current?.isConnected()) {
|
if (!streamConnectionRef.current?.isConnected()) {
|
||||||
connectToPlanningStream(session.id);
|
connectToPlanningStream(session.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1576,6 +1576,346 @@ describe("PlanningModeModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Initial-turn reasoning visibility (FN-3274)", () => {
|
||||||
|
it("preserves reasoning in conversation history when first question arrives after thinking", async () => {
|
||||||
|
let streamHandlers: any;
|
||||||
|
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||||
|
streamHandlers = handlers;
|
||||||
|
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"));
|
||||||
|
|
||||||
|
// Wait for loading state to appear
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Generating next question...")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate thinking output arriving during loading
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onThinking?.("Analyzing the plan requirements...");
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("AI is thinking...")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transition to question view
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onQuestion?.(mockQuestion);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Question should be visible
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("What is the scope?")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The reasoning should now be in conversation history as an expandable entry
|
||||||
|
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();
|
||||||
|
|
||||||
|
// avoid dangling handlers reference lint
|
||||||
|
expect(streamHandlers).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves reasoning in conversation history when summary arrives after thinking", async () => {
|
||||||
|
let streamHandlers: any;
|
||||||
|
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||||
|
streamHandlers = handlers;
|
||||||
|
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("Generating next question...")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate thinking output arriving
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onThinking?.("Finalizing the planning summary...");
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("AI is thinking...")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transition directly to summary view
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onSummary?.(mockSummary);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Planning Complete!")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The reasoning should be visible in the Q&A disclosure
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Show user Q&A" }));
|
||||||
|
await waitFor(() => {
|
||||||
|
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("Finalizing the planning summary...")).toBeDefined();
|
||||||
|
|
||||||
|
expect(streamHandlers).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restores persisted thinkingOutput as conversation history when resuming awaiting_input session", async () => {
|
||||||
|
mockConnectPlanningStream.mockImplementationOnce(() => ({
|
||||||
|
close: vi.fn(),
|
||||||
|
isConnected: vi.fn().mockReturnValue(true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resumedQuestion: PlanningQuestion = {
|
||||||
|
id: "q-current",
|
||||||
|
type: "text",
|
||||||
|
question: "What should we prioritize next?",
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoredHistory = [
|
||||||
|
{
|
||||||
|
question: {
|
||||||
|
id: "q1",
|
||||||
|
type: "single_select",
|
||||||
|
question: "What scope?",
|
||||||
|
options: [{ id: "small", label: "Small" }],
|
||||||
|
},
|
||||||
|
response: { q1: "small" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockFetchAiSession.mockResolvedValueOnce({
|
||||||
|
id: "session-awaiting-reasoning",
|
||||||
|
type: "planning",
|
||||||
|
status: "awaiting_input",
|
||||||
|
title: "Resume with reasoning",
|
||||||
|
inputPayload: JSON.stringify({ initialPlan: "Build planning with reasoning" }),
|
||||||
|
conversationHistory: JSON.stringify(restoredHistory),
|
||||||
|
currentQuestion: JSON.stringify(resumedQuestion),
|
||||||
|
result: null,
|
||||||
|
thinkingOutput: "Server-side reasoning captured during generation",
|
||||||
|
error: null,
|
||||||
|
projectId: null,
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<PlanningModeModal
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
onTaskCreated={mockOnTaskCreated}
|
||||||
|
onTasksCreated={vi.fn()}
|
||||||
|
tasks={mockTasks}
|
||||||
|
resumeSessionId="session-awaiting-reasoning"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("What should we prioritize next?")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The persisted thinkingOutput should appear as a conversation history entry
|
||||||
|
const history = screen.getByTestId("conversation-history");
|
||||||
|
expect(history).toBeDefined();
|
||||||
|
|
||||||
|
// Should show the existing Q&A plus the AI Reasoning entry
|
||||||
|
expect(screen.getByText("What scope?")).toBeDefined();
|
||||||
|
expect(screen.getByText("AI Reasoning")).toBeDefined();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Show AI reasoning/i }));
|
||||||
|
expect(screen.getByText("Server-side reasoning captured during generation")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not create duplicate reasoning entries on repeated transitions", async () => {
|
||||||
|
let streamHandlers: any;
|
||||||
|
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||||
|
streamHandlers = handlers;
|
||||||
|
return {
|
||||||
|
close: vi.fn(),
|
||||||
|
isConnected: vi.fn().mockReturnValue(true),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondQuestion: PlanningQuestion = {
|
||||||
|
id: "q-second",
|
||||||
|
type: "text",
|
||||||
|
question: "Any additional requirements?",
|
||||||
|
};
|
||||||
|
|
||||||
|
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("Generating next question...")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit thinking then question
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onThinking?.("First reasoning block");
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onQuestion?.(mockQuestion);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("What is the scope?")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Answer the question
|
||||||
|
fireEvent.click(screen.getByText("Medium"));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRespondToPlanning).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate thinking for second question then emit second question
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onThinking?.("Second reasoning block");
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onQuestion?.(secondQuestion);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Any additional requirements?")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Conversation history should contain both reasoning entries without duplicates
|
||||||
|
const history = screen.getByTestId("conversation-history");
|
||||||
|
expect(history).toBeDefined();
|
||||||
|
|
||||||
|
// Should have Q1, reasoning1, reasoning2 entries
|
||||||
|
const reasoningButtons = screen.getAllByRole("button", { name: /Show AI reasoning/i });
|
||||||
|
// First reasoning button should be next to Q1, second should be standalone
|
||||||
|
// There should be exactly 2 reasoning entries (not duplicated)
|
||||||
|
expect(reasoningButtons.length).toBe(2);
|
||||||
|
|
||||||
|
expect(streamHandlers).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves reasoning when answer submission transitions back to loading then question", async () => {
|
||||||
|
let streamHandlers: any;
|
||||||
|
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||||
|
streamHandlers = handlers;
|
||||||
|
return {
|
||||||
|
close: vi.fn(),
|
||||||
|
isConnected: vi.fn().mockReturnValue(true),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondQuestion: PlanningQuestion = {
|
||||||
|
id: "q-requirements",
|
||||||
|
type: "text",
|
||||||
|
question: "What are the key requirements?",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRespondToPlanning.mockImplementation(async () => {
|
||||||
|
// Simulate thinking then second question via the existing stream
|
||||||
|
setTimeout(() => {
|
||||||
|
streamHandlers?.onThinking?.("Thinking about requirements...");
|
||||||
|
streamHandlers?.onQuestion?.(secondQuestion);
|
||||||
|
}, 10);
|
||||||
|
return { sessionId: "session-123", currentQuestion: null, summary: null };
|
||||||
|
});
|
||||||
|
|
||||||
|
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"));
|
||||||
|
|
||||||
|
// Wait for first thinking and question
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Generating next question...")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onThinking?.("Initial analysis...");
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onQuestion?.(mockQuestion);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("What is the scope?")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Answer the first question
|
||||||
|
fireEvent.click(screen.getByText("Medium"));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||||
|
|
||||||
|
// Wait for second question to arrive
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("What are the key requirements?")).toBeDefined();
|
||||||
|
}, { timeout: 3000 });
|
||||||
|
|
||||||
|
// Conversation history should contain the first Q&A pair and initial reasoning
|
||||||
|
const history = screen.getByTestId("conversation-history");
|
||||||
|
expect(history).toBeDefined();
|
||||||
|
expect(screen.getByText("What is the scope?")).toBeDefined();
|
||||||
|
expect(screen.getByText("Medium")).toBeDefined();
|
||||||
|
|
||||||
|
expect(streamHandlers).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Question view", () => {
|
describe("Question view", () => {
|
||||||
it("renders single_select question with options", async () => {
|
it("renders single_select question with options", async () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
|
|||||||
Reference in New Issue
Block a user