fix(dashboard): recover finished planning sessions instead of retry dead-ends
Reopening a complete plan no longer shows "still being prepared" when the validated payload marker is missing. Generation Retry that hits "already been validated" refreshes into create-retry or plan review.
This commit is contained in:
7
.changeset/fix-planning-complete-reopen.md
Normal file
7
.changeset/fix-planning-complete-reopen.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Fix Planning reopen after a finished session so Retry no longer dead-ends.
|
||||||
|
category: fix
|
||||||
|
dev: Treat status=complete as terminal; recover create-retry/task-created/plan-review on load and when generation retry reports already-validated.
|
||||||
@@ -182,24 +182,66 @@ type PlanningGenerationActivity = "initial_plan" | "plan_update" | "question";
|
|||||||
/**
|
/**
|
||||||
* FNXC:PlanningMode 2026-07-20-00:00:
|
* FNXC:PlanningMode 2026-07-20-00:00:
|
||||||
* A persisted planning `result` is an evolving running plan, not proof that the interview ended.
|
* A persisted planning `result` is an evolving running plan, not proof that the interview ended.
|
||||||
* Only the explicit Validate action writes this durable marker, so reload and poll paths must use
|
* Only the explicit Validate action writes the durable terminal markers, so reload and poll paths
|
||||||
* it before exposing terminal summary/create-task UI.
|
* must use them before exposing create-task UI.
|
||||||
|
*
|
||||||
|
* FNXC:PlanningMode 2026-07-24-05:45:
|
||||||
|
* `status === "complete"` is written only by validateSession, so it is authoritative terminal
|
||||||
|
* evidence even when a stale/malformed inputPayload omits `validated: true` (that mismatch was
|
||||||
|
* stranding reopen on "still being prepared", then Retry hit "already been validated"). Prefer
|
||||||
|
* status, then the payload marker, and always recover complete rows to task-created / create-retry
|
||||||
|
* / plan-review rather than a generation-retry dead end.
|
||||||
*/
|
*/
|
||||||
function isValidatedPlanningSession(session: { inputPayload?: string | null }): boolean {
|
function parsePlanningInputPayload(session: { inputPayload?: string | null }): {
|
||||||
|
validated: boolean;
|
||||||
|
createdTaskId?: string;
|
||||||
|
} {
|
||||||
try {
|
try {
|
||||||
const payload: unknown = JSON.parse(session.inputPayload ?? "");
|
const payload = JSON.parse(session.inputPayload ?? "{}") as {
|
||||||
/*
|
validated?: unknown;
|
||||||
FNXC:PlanningMode 2026-07-20-01:15:
|
createdTaskId?: unknown;
|
||||||
Terminal Planning UI is an explicit user-validation privilege, not a legacy-session inference.
|
};
|
||||||
Missing or malformed persistence may contain a running plan, so only a durable `validated: true`
|
if (typeof payload !== "object" || payload === null) {
|
||||||
marker can reveal SummaryView and task-creation actions after reload, polling, or SSE updates.
|
return { validated: false };
|
||||||
*/
|
}
|
||||||
return typeof payload === "object" && payload !== null && (payload as { validated?: unknown }).validated === true;
|
return {
|
||||||
|
validated: payload.validated === true,
|
||||||
|
createdTaskId: typeof payload.createdTaskId === "string" ? payload.createdTaskId : undefined,
|
||||||
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return { validated: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CompletePlanningResume =
|
||||||
|
| { kind: "task_created"; taskId: string; summary: PlanningSummary }
|
||||||
|
| { kind: "create_retry"; summary: PlanningSummary }
|
||||||
|
| { kind: "plan_review"; summary: PlanningSummary }
|
||||||
|
| { kind: "unrecoverable" };
|
||||||
|
|
||||||
|
function resolveCompletePlanningResume(
|
||||||
|
session: { status?: string; result?: string | null; inputPayload?: string | null },
|
||||||
|
summaryFallback?: PlanningSummary | null,
|
||||||
|
): CompletePlanningResume {
|
||||||
|
let summary: PlanningSummary | null = summaryFallback ?? null;
|
||||||
|
if (!summary && session.result) {
|
||||||
|
try {
|
||||||
|
summary = normalizePlanningSummary(JSON.parse(session.result) as PlanningSummary);
|
||||||
|
} catch {
|
||||||
|
summary = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!summary) return { kind: "unrecoverable" };
|
||||||
|
|
||||||
|
const { validated, createdTaskId } = parsePlanningInputPayload(session);
|
||||||
|
const terminal = session.status === "complete" || validated;
|
||||||
|
if (terminal) {
|
||||||
|
if (createdTaskId) return { kind: "task_created", taskId: createdTaskId, summary };
|
||||||
|
return { kind: "create_retry", summary };
|
||||||
|
}
|
||||||
|
return { kind: "plan_review", summary };
|
||||||
|
}
|
||||||
|
|
||||||
function getExamplePlans(t: TFunction<"app">): string[] {
|
function getExamplePlans(t: TFunction<"app">): string[] {
|
||||||
return [
|
return [
|
||||||
t("planning.examplePlan1", "Build a user authentication system with login and signup"),
|
t("planning.examplePlan1", "Build a user authentication system with login and signup"),
|
||||||
@@ -993,22 +1035,32 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
});
|
});
|
||||||
setStreamingOutput("");
|
setStreamingOutput("");
|
||||||
} else if (session.status === "complete" && session.result) {
|
} else if (session.status === "complete" && session.result) {
|
||||||
const summary = normalizePlanningSummary(JSON.parse(session.result) as PlanningSummary);
|
const resume = resolveCompletePlanningResume(session);
|
||||||
setRunningSummary(summary);
|
if (resume.kind === "unrecoverable") {
|
||||||
if (isValidatedPlanningSession(session)) {
|
|
||||||
resetPlanningAutoRetryBudget();
|
|
||||||
const inputPayload = JSON.parse(session.inputPayload ?? "{}") as { createdTaskId?: unknown };
|
|
||||||
if (typeof inputPayload.createdTaskId === "string") {
|
|
||||||
setView({ type: "task_created", taskId: inputPayload.createdTaskId });
|
|
||||||
} else {
|
|
||||||
setView({ type: "create_retry", session: { sessionId, currentQuestion: null, summary }, summary, errorMessage: t("planning.retryCreate", "Retry create") });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setView({
|
setView({
|
||||||
type: "error",
|
type: "error",
|
||||||
session: { sessionId, currentQuestion: null, summary },
|
session: { sessionId, currentQuestion: null, summary: null },
|
||||||
errorMessage: t("planning.awaitingValidationState", "This plan is still being prepared. Retry to continue the interview."),
|
errorMessage: t("planning.sessionUnrecoverableState", "This session could not be restored. Retry to continue the interview."),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
setRunningSummary(resume.summary);
|
||||||
|
resetPlanningAutoRetryBudget();
|
||||||
|
if (resume.kind === "task_created") {
|
||||||
|
setView({ type: "task_created", taskId: resume.taskId });
|
||||||
|
} else if (resume.kind === "create_retry") {
|
||||||
|
setView({
|
||||||
|
type: "create_retry",
|
||||||
|
session: { sessionId, currentQuestion: null, summary: resume.summary },
|
||||||
|
summary: resume.summary,
|
||||||
|
errorMessage: t("planning.retryCreate", "Retry create"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setView({
|
||||||
|
type: "plan_review",
|
||||||
|
session: { sessionId, currentQuestion: null, summary: resume.summary },
|
||||||
|
summary: resume.summary,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setStreamingOutput("");
|
setStreamingOutput("");
|
||||||
} else if (session.status === "error") {
|
} else if (session.status === "error") {
|
||||||
@@ -1359,10 +1411,21 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
let retryError: unknown = err;
|
let retryError: unknown = err;
|
||||||
const retryErrorMessage = getErrorMessage(err) || "";
|
const retryErrorMessage = getErrorMessage(err) || "";
|
||||||
|
|
||||||
// FNXC:PlanningTurnAdmission 2026-07-22-21:00: a retry rejected because another turn
|
/*
|
||||||
// already holds the session's turn slot means work is in progress — rejoin it via the
|
FNXC:PlanningTurnAdmission 2026-07-22-21:00:
|
||||||
// same session-refresh path instead of surfacing a terminal error.
|
A retry rejected because another turn already holds the session's turn slot means work is
|
||||||
if (retryErrorMessage.includes("not in an error state") || retryErrorMessage.includes("already in progress")) {
|
in progress — rejoin it via the same session-refresh path instead of a terminal error.
|
||||||
|
|
||||||
|
FNXC:PlanningMode 2026-07-24-05:45:
|
||||||
|
"already been validated" is the same class of mismatch: generation retry is the wrong tool
|
||||||
|
for a finished plan. Refresh the durable row and route to create-retry / task-created /
|
||||||
|
plan-review instead of echoing the server exception.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
retryErrorMessage.includes("not in an error state")
|
||||||
|
|| retryErrorMessage.includes("already in progress")
|
||||||
|
|| retryErrorMessage.includes("already been validated")
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await fetchAiSession(retryTarget.sessionId);
|
const session = await fetchAiSession(retryTarget.sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -1379,49 +1442,69 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
connectToPlanningStream(session.id);
|
connectToPlanningStream(session.id);
|
||||||
setView({ type: "loading" });
|
setView({ type: "loading" });
|
||||||
} else if (session.status === "awaiting_input") {
|
} else if (session.status === "awaiting_input") {
|
||||||
if (!session.currentQuestion) {
|
|
||||||
throw new Error("Planning session is awaiting input but has no current question.");
|
|
||||||
}
|
|
||||||
resetPlanningAutoRetryBudget();
|
resetPlanningAutoRetryBudget();
|
||||||
const question = normalizeQuestionOptions(JSON.parse(session.currentQuestion) as PlanningQuestion);
|
|
||||||
clearPlanningDescription(projectId);
|
clearPlanningDescription(projectId);
|
||||||
setView({
|
const summary = session.result
|
||||||
type: "question",
|
? normalizePlanningSummary(JSON.parse(session.result) as PlanningSummary)
|
||||||
session: { sessionId: session.id, currentQuestion: question, summary: null },
|
: retryTarget.summary;
|
||||||
});
|
if (summary) setRunningSummary(summary);
|
||||||
if (session.thinkingOutput) {
|
if (session.currentQuestion) {
|
||||||
const trimmed = session.thinkingOutput.trim();
|
const question = normalizeQuestionOptions(JSON.parse(session.currentQuestion) as PlanningQuestion);
|
||||||
if (trimmed) {
|
setWorkspaceQuestion(question);
|
||||||
setConversationHistory((prev) => {
|
setView({
|
||||||
const lastEntry = prev[prev.length - 1];
|
type: "question",
|
||||||
if (lastEntry?.thinkingOutput === trimmed) return prev;
|
session: { sessionId: session.id, currentQuestion: question, summary },
|
||||||
return [...prev, { thinkingOutput: trimmed }];
|
});
|
||||||
});
|
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);
|
}
|
||||||
|
} else if (summary) {
|
||||||
|
/*
|
||||||
|
FNXC:PlanningMode 2026-07-24-05:45:
|
||||||
|
Plan-review rows are awaiting_input with a running plan and no current question.
|
||||||
|
Treating that as fatal forced Retry into an unrecoverable error after stop/proceed.
|
||||||
|
*/
|
||||||
|
setWorkspaceQuestion(null);
|
||||||
|
setView({
|
||||||
|
type: "plan_review",
|
||||||
|
session: { sessionId: session.id, currentQuestion: null, summary },
|
||||||
|
summary,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Planning session is awaiting input but has no current question or plan.");
|
||||||
}
|
}
|
||||||
} else if (session.status === "complete") {
|
} else if (session.status === "complete") {
|
||||||
if (!session.result) {
|
const resume = resolveCompletePlanningResume(session, retryTarget.summary);
|
||||||
|
if (resume.kind === "unrecoverable") {
|
||||||
throw new Error("Planning session is complete but has no result.");
|
throw new Error("Planning session is complete but has no result.");
|
||||||
}
|
}
|
||||||
const summary = normalizePlanningSummary(JSON.parse(session.result) as PlanningSummary);
|
resetPlanningAutoRetryBudget();
|
||||||
setRunningSummary(summary);
|
clearPlanningDescription(projectId);
|
||||||
if (isValidatedPlanningSession(session)) {
|
setRunningSummary(resume.summary);
|
||||||
resetPlanningAutoRetryBudget();
|
if (resume.kind === "task_created") {
|
||||||
clearPlanningDescription(projectId);
|
setView({ type: "task_created", taskId: resume.taskId });
|
||||||
const inputPayload = JSON.parse(session.inputPayload ?? "{}") as { createdTaskId?: unknown };
|
} else if (resume.kind === "create_retry") {
|
||||||
if (typeof inputPayload.createdTaskId === "string") {
|
setView({
|
||||||
setView({ type: "task_created", taskId: inputPayload.createdTaskId });
|
type: "create_retry",
|
||||||
} else {
|
session: { sessionId: session.id, currentQuestion: null, summary: resume.summary },
|
||||||
setView({ type: "create_retry", session: { sessionId: session.id, currentQuestion: null, summary }, summary, errorMessage: t("planning.retryCreate", "Retry create") });
|
summary: resume.summary,
|
||||||
}
|
errorMessage: t("planning.retryCreate", "Retry create"),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setView({
|
setView({
|
||||||
type: "error",
|
type: "plan_review",
|
||||||
session: { sessionId: session.id, currentQuestion: null, summary },
|
session: { sessionId: session.id, currentQuestion: null, summary: resume.summary },
|
||||||
errorMessage: t("planning.awaitingValidationState", "This plan is still being prepared. Retry to continue the interview."),
|
summary: resume.summary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (session.status === "error") {
|
} else if (session.status === "error") {
|
||||||
@@ -1841,23 +1924,33 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (session.status === "complete" && session.result) {
|
} else if (session.status === "complete" && session.result) {
|
||||||
const summary = persistedRunningSummary ?? normalizePlanningSummary(JSON.parse(session.result));
|
const resume = resolveCompletePlanningResume(session, persistedRunningSummary);
|
||||||
setRunningSummary(summary);
|
if (resume.kind === "unrecoverable") {
|
||||||
if (isValidatedPlanningSession(session)) {
|
|
||||||
const createdTaskId = inputPayload && typeof inputPayload.createdTaskId === "string" ? inputPayload.createdTaskId : undefined;
|
|
||||||
resetPlanningAutoRetryBudget();
|
|
||||||
clearPlanningDescription(projectId);
|
|
||||||
if (createdTaskId) {
|
|
||||||
setView({ type: "task_created", taskId: createdTaskId });
|
|
||||||
} else {
|
|
||||||
setView({ type: "create_retry", session: { sessionId, currentQuestion: null, summary }, summary, errorMessage: t("planning.retryCreate", "Retry create") });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setView({
|
setView({
|
||||||
type: "error",
|
type: "error",
|
||||||
session: { sessionId, currentQuestion: null, summary },
|
session: { sessionId, currentQuestion: null, summary: persistedRunningSummary },
|
||||||
errorMessage: t("planning.awaitingValidationState", "This plan is still being prepared. Retry to continue the interview."),
|
errorMessage: t("planning.sessionUnrecoverableState", "This session could not be restored. Retry to continue the interview."),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
setRunningSummary(resume.summary);
|
||||||
|
resetPlanningAutoRetryBudget();
|
||||||
|
clearPlanningDescription(projectId);
|
||||||
|
if (resume.kind === "task_created") {
|
||||||
|
setView({ type: "task_created", taskId: resume.taskId });
|
||||||
|
} else if (resume.kind === "create_retry") {
|
||||||
|
setView({
|
||||||
|
type: "create_retry",
|
||||||
|
session: { sessionId, currentQuestion: null, summary: resume.summary },
|
||||||
|
summary: resume.summary,
|
||||||
|
errorMessage: t("planning.retryCreate", "Retry create"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setView({
|
||||||
|
type: "plan_review",
|
||||||
|
session: { sessionId, currentQuestion: null, summary: resume.summary },
|
||||||
|
summary: resume.summary,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (session.status === "generating") {
|
} else if (session.status === "generating") {
|
||||||
setView({ type: "loading" });
|
setView({ type: "loading" });
|
||||||
|
|||||||
@@ -216,7 +216,12 @@ describe("PlanningModeModal autosize", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not expose the removed final review for an unvalidated completed session", async () => {
|
it("resumes a complete session without a durable validated payload marker as create-retry", async () => {
|
||||||
|
/*
|
||||||
|
FNXC:PlanningMode 2026-07-24-05:45:
|
||||||
|
status=complete is only written by validateSession. A missing inputPayload.validated flag
|
||||||
|
must not strand reopen on "still being prepared" — route to create-retry so Proceed can finish.
|
||||||
|
*/
|
||||||
mockFetchAiSession.mockResolvedValueOnce({
|
mockFetchAiSession.mockResolvedValueOnce({
|
||||||
id: "session-complete-1",
|
id: "session-complete-1",
|
||||||
type: "planning",
|
type: "planning",
|
||||||
@@ -250,7 +255,51 @@ describe("PlanningModeModal autosize", () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await screen.findByRole("alert")).toHaveTextContent("This plan is still being prepared");
|
expect(await screen.findByTestId("planning-create-retry")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "Retry create" })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("This plan is still being prepared")).toBeNull();
|
||||||
expect(screen.queryByTestId("planning-description-markdown-toggle")).toBeNull();
|
expect(screen.queryByTestId("planning-description-markdown-toggle")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens the linked task when a complete session already has a createdTaskId", async () => {
|
||||||
|
mockFetchAiSession.mockResolvedValueOnce({
|
||||||
|
id: "session-complete-linked",
|
||||||
|
type: "planning",
|
||||||
|
status: "complete",
|
||||||
|
title: "Linked planning output",
|
||||||
|
inputPayload: JSON.stringify({
|
||||||
|
initialPlan: "Build resilient planning resume",
|
||||||
|
validated: true,
|
||||||
|
createdTaskId: "FN-9001",
|
||||||
|
}),
|
||||||
|
conversationHistory: "[]",
|
||||||
|
currentQuestion: null,
|
||||||
|
result: JSON.stringify({
|
||||||
|
title: "Linked planning output",
|
||||||
|
description: "Recovered summary",
|
||||||
|
suggestedSize: "M",
|
||||||
|
suggestedDependencies: [],
|
||||||
|
keyDeliverables: ["Done"],
|
||||||
|
}),
|
||||||
|
thinkingOutput: "",
|
||||||
|
error: null,
|
||||||
|
projectId: null,
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<PlanningModeModal
|
||||||
|
isOpen={true}
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onTaskCreated={vi.fn()}
|
||||||
|
onTasksCreated={vi.fn()}
|
||||||
|
tasks={mockTasks}
|
||||||
|
resumeSessionId="session-complete-linked"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await screen.findByText("FN-9001")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("planning-create-retry")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -987,4 +987,56 @@ describe("PlanningModeModal sequential flow", () => {
|
|||||||
expect(await screen.findByTestId("planning-create-retry")).toBeInTheDocument();
|
expect(await screen.findByTestId("planning-create-retry")).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: "Proceed with plan" })).toBeNull();
|
expect(screen.queryByRole("button", { name: "Proceed with plan" })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes generation retry away from already-validated sessions into create-retry", async () => {
|
||||||
|
/*
|
||||||
|
FNXC:PlanningMode 2026-07-24-05:45:
|
||||||
|
Auto/manual generation retry on a finished plan used to echo "already been validated".
|
||||||
|
Reject retry, re-fetch the complete row, and land on create-retry.
|
||||||
|
*/
|
||||||
|
mockRetryPlanningSession.mockRejectedValue(new Error("Planning session has already been validated"));
|
||||||
|
mockFetchAiSession
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
...base,
|
||||||
|
status: "error",
|
||||||
|
currentQuestion: null,
|
||||||
|
result: JSON.stringify(mockSummary),
|
||||||
|
error: "stream failed",
|
||||||
|
inputPayload: JSON.stringify({ validated: true }),
|
||||||
|
})
|
||||||
|
.mockResolvedValue({
|
||||||
|
...base,
|
||||||
|
status: "complete",
|
||||||
|
currentQuestion: null,
|
||||||
|
result: JSON.stringify(mockSummary),
|
||||||
|
inputPayload: JSON.stringify({ validated: true }),
|
||||||
|
});
|
||||||
|
renderSession();
|
||||||
|
expect(await screen.findByTestId("planning-create-retry")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Planning session has already been validated")).toBeNull();
|
||||||
|
expect(mockRetryPlanningSession).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restores plan review when awaiting_input has a plan but no current question after retry refresh", async () => {
|
||||||
|
mockRetryPlanningSession.mockRejectedValue(new Error("Planning session session-1 is not in an error state"));
|
||||||
|
mockFetchAiSession
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
...base,
|
||||||
|
status: "error",
|
||||||
|
currentQuestion: null,
|
||||||
|
result: JSON.stringify(mockSummary),
|
||||||
|
error: "stream failed",
|
||||||
|
inputPayload: "{}",
|
||||||
|
})
|
||||||
|
.mockResolvedValue({
|
||||||
|
...base,
|
||||||
|
status: "awaiting_input",
|
||||||
|
currentQuestion: null,
|
||||||
|
result: JSON.stringify(mockSummary),
|
||||||
|
inputPayload: "{}",
|
||||||
|
});
|
||||||
|
renderSession();
|
||||||
|
expect(await screen.findByTestId("planning-plan-review")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "Proceed with plan" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user