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();