From 907c051844bcb44ca7549dafb80c91b57ff0f869 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 5 Aug 2026 00:55:03 -0700 Subject: [PATCH] FN-8803: fix TaskDetailModal prompt refresh coverage Align TaskDetailModal refresh tests with the narrow prompt API. - Exercise visible plan refreshes through fetchTaskPrompt - Reset prompt and detail API mocks to preserve Promise contracts Files changed: .../TaskDetailModal.definition-actions.test.tsx | 57 +++++++++++++--------- .../__tests__/TaskDetailModal.test-helpers.ts | 19 +++++--- 2 files changed, 47 insertions(+), 29 deletions(-) Fusion-Task-Id: FN-8803 Fusion-Task-Lineage: e2539c24-377b-48bf-81d1-80206da7071c Co-authored-by: Fusion (runfusion.ai) --- ...askDetailModal.definition-actions.test.tsx | 57 +++++++++++-------- .../__tests__/TaskDetailModal.test-helpers.ts | 19 +++++-- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx index e92de2a42b..e3bc7f0fb8 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx @@ -1801,38 +1801,49 @@ describe("TaskDetailModal", () => { it("refreshes on show, re-entry, and visible planning polls", async () => { vi.useFakeTimers(); - const { fetchTaskDetail } = await import("../../api"); + const { fetchTaskDetail, fetchTaskPrompt } = await import("../../api"); const mockFetchDetail = vi.mocked(fetchTaskDetail); - mockFetchDetail.mockReset(); - mockFetchDetail - .mockResolvedValueOnce(makeTask({ id: "FN-fresh", column: "triage", status: "planning", prompt: "# First revision" })) - .mockResolvedValueOnce(makeTask({ id: "FN-fresh", column: "triage", status: "planning", prompt: "# Polled revision" })) - .mockResolvedValueOnce(makeTask({ id: "FN-fresh", column: "triage", status: "planning", prompt: "# Re-entered revision" })); + const mockFetchPrompt = vi.mocked(fetchTaskPrompt); + mockFetchDetail.mockClear(); + mockFetchPrompt.mockReset(); + mockFetchPrompt + .mockResolvedValueOnce({ id: "FN-fresh", prompt: "# First revision" }) + .mockResolvedValueOnce({ id: "FN-fresh", prompt: "# Polled revision" }) + .mockResolvedValueOnce({ id: "FN-fresh", prompt: "# Re-entered revision" }); - render(); + render(); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); expect(screen.getByText("First revision")).toBeTruthy(); + expect(mockFetchPrompt).toHaveBeenCalledWith("FN-fresh", "project-fresh"); + expect(mockFetchDetail).not.toHaveBeenCalled(); + await act(async () => { await vi.advanceTimersByTimeAsync(5_000); }); expect(screen.getByText("Polled revision")).toBeTruthy(); + expect(mockFetchPrompt).toHaveBeenCalledTimes(2); + expect(mockFetchDetail).not.toHaveBeenCalled(); fireEvent.click(screen.getByText("Activity")); await act(async () => { await vi.advanceTimersByTimeAsync(15_000); }); - expect(mockFetchDetail).toHaveBeenCalledTimes(2); + expect(mockFetchPrompt).toHaveBeenCalledTimes(2); + expect(mockFetchDetail).not.toHaveBeenCalled(); fireEvent.click(screen.getByText("Plan")); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); expect(screen.getByText("Re-entered revision")).toBeTruthy(); + expect(mockFetchPrompt).toHaveBeenCalledTimes(3); + expect(mockFetchPrompt).toHaveBeenLastCalledWith("FN-fresh", "project-fresh"); + expect(mockFetchDetail).not.toHaveBeenCalled(); }); it("keeps an inline edit buffer stable while a Plan Review refresh arrives", async () => { vi.useFakeTimers(); - const { fetchTaskDetail } = await import("../../api"); - const mockFetchDetail = vi.mocked(fetchTaskDetail); - mockFetchDetail.mockReset(); - mockFetchDetail - .mockResolvedValueOnce(makeTask({ id: "FN-edit", column: "todo", prompt: "# Server revision", workflowStepResults: [{ workflowStepId: "plan-review", status: "pending", startedAt: "2026-08-03T02:00:00Z" }] })) - .mockResolvedValueOnce(makeTask({ id: "FN-edit", column: "todo", prompt: "# New server revision", workflowStepResults: [{ workflowStepId: "plan-review", status: "pending", startedAt: "2026-08-03T02:00:00Z" }] })); + const { fetchTaskPrompt } = await import("../../api"); + const mockFetchPrompt = vi.mocked(fetchTaskPrompt); + mockFetchPrompt.mockReset(); + mockFetchPrompt + .mockResolvedValueOnce({ id: "FN-edit", prompt: "# Server revision" }) + .mockResolvedValueOnce({ id: "FN-edit", prompt: "# New server revision" }); render(); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); @@ -1843,24 +1854,24 @@ describe("TaskDetailModal", () => { fireEvent.change(textarea, { target: { value: "# Local operator edit" } }); const sameTextarea = textarea; await act(async () => { await vi.advanceTimersByTimeAsync(5_000); }); - expect(mockFetchDetail).toHaveBeenCalledTimes(2); + expect(mockFetchPrompt).toHaveBeenCalledTimes(2); expect(document.querySelector(".spec-editor-textarea")).toBe(sameTextarea); expect(textarea.value).toBe("# Local operator edit"); }); it("ignores a late planning response after the task changes", async () => { - const { fetchTaskDetail } = await import("../../api"); - const mockFetchDetail = vi.mocked(fetchTaskDetail); - let resolveFirst: (detail: TaskDetail) => void = () => {}; - mockFetchDetail.mockReset(); - mockFetchDetail - .mockImplementationOnce(() => new Promise((resolve) => { resolveFirst = resolve; })) - .mockResolvedValueOnce(makeTask({ id: "FN-current", column: "triage", status: "planning", prompt: "# Current task" })); + const { fetchTaskPrompt } = await import("../../api"); + const mockFetchPrompt = vi.mocked(fetchTaskPrompt); + let resolveFirst: (response: { id: string; prompt?: string }) => void = () => {}; + mockFetchPrompt.mockReset(); + mockFetchPrompt + .mockImplementationOnce(() => new Promise<{ id: string; prompt?: string }>((resolve) => { resolveFirst = resolve; })) + .mockResolvedValueOnce({ id: "FN-current", prompt: "# Current task" }); const props = { initialTab: "definition" as const, onMoveTask: noopMove, onDeleteTask: noopDelete, onMergeTask: noopMerge, onOpenDetail: noopOpenDetail, addToast: noop }; const view = render(); view.rerender(); await waitFor(() => expect(screen.getByText("Current task")).toBeTruthy()); - await act(async () => { resolveFirst(makeTask({ id: "FN-old", column: "triage", status: "planning", prompt: "# Stale task" })); }); + await act(async () => { resolveFirst({ id: "FN-old", prompt: "# Stale task" }); }); expect(screen.queryByText("Stale task")).toBeNull(); expect(screen.getByText("Current task")).toBeTruthy(); }); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.test-helpers.ts b/packages/dashboard/app/components/__tests__/TaskDetailModal.test-helpers.ts index ab48630375..c4c48d3f51 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.test-helpers.ts +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.test-helpers.ts @@ -230,11 +230,11 @@ export function makeTask(overrides: Partial = {}): TaskDetail { } /* -FNXC:DashboardTests 2026-08-04-15:05: -Definition refresh always chains `fetchTaskDetail(...).then(...)`; a bare Vitest -mock returns undefined and is not a valid stand-in for the production Promise contract. -Use this reset helper after an intentional override so later tests regain a resolved -TaskDetail default instead of leaking an impossible undefined response. +FNXC:DashboardTests 2026-08-05-07:32: +FN-8803 confirms initial slim-task hydration uses `fetchTaskDetail`, while visible +Definition refresh uses `fetchTaskPrompt`. Both mocks must retain Promise-returning +defaults after an individual test resets them, so later tests cannot leak an impossible +undefined response into either production request boundary. */ export async function resetTaskDetailFetchMock(): Promise { const { fetchTaskDetail } = await import("../../api"); @@ -242,6 +242,12 @@ export async function resetTaskDetailFetchMock(): Promise { vi.mocked(fetchTaskDetail).mockResolvedValue(makeTask()); } +export async function resetTaskPromptFetchMock(): Promise { + const { fetchTaskPrompt } = await import("../../api"); + vi.mocked(fetchTaskPrompt).mockReset(); + vi.mocked(fetchTaskPrompt).mockResolvedValue({ id: "FN-099", prompt: "# Task FN-099" }); +} + export const noop = vi.fn(); export const noopMove = vi.fn(async () => ({}) as Task); export const noopDelete = vi.fn(async () => ({}) as Task); @@ -274,8 +280,9 @@ export function loadDashboardCss(): string { export function setupTaskDetailModalHooks(): void { beforeEach(async () => { - // FNXC:DashboardTests 2026-08-04-15:05: Every TaskDetailModal suite begins with the Promise-returning detail contract; tests layer pending/rejected/custom responses after this reset. + // FNXC:DashboardTests 2026-08-05-07:32: Every TaskDetailModal suite begins with Promise-returning full-detail and narrow-prompt contracts; tests layer pending/rejected/custom responses after these resets. await resetTaskDetailFetchMock(); + await resetTaskPromptFetchMock(); mockConfirm.mockReset(); mockConfirmWithChoice.mockReset(); mockConfirmWithCheckbox.mockReset();