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) <noreply@runfusion.ai>
This commit is contained in:
@@ -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(<TaskDetailContent task={makeTask({ id: "FN-fresh", column: "triage", status: "planning", prompt: "" })} initialTab="definition" onMoveTask={noopMove} onDeleteTask={noopDelete} onMergeTask={noopMerge} onOpenDetail={noopOpenDetail} addToast={noop} />);
|
||||
render(<TaskDetailContent task={makeTask({ id: "FN-fresh", column: "triage", status: "planning", prompt: "" })} projectId="project-fresh" initialTab="definition" onMoveTask={noopMove} onDeleteTask={noopDelete} onMergeTask={noopMerge} onOpenDetail={noopOpenDetail} addToast={noop} />);
|
||||
|
||||
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(<TaskDetailContent task={makeTask({ id: "FN-edit", column: "todo", prompt: "# Initial", workflowStepResults: [{ workflowStepId: "plan-review", status: "pending", startedAt: "2026-08-03T02:00:00Z" }] })} initialTab="definition" onMoveTask={noopMove} onDeleteTask={noopDelete} onMergeTask={noopMerge} onOpenDetail={noopOpenDetail} addToast={noop} />);
|
||||
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<TaskDetail>((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(<TaskDetailContent {...props} task={makeTask({ id: "FN-old", column: "triage", status: "planning", prompt: "# Old task" })} />);
|
||||
view.rerender(<TaskDetailContent {...props} task={makeTask({ id: "FN-current", column: "triage", status: "planning", prompt: "" })} />);
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -230,11 +230,11 @@ export function makeTask(overrides: Partial<TaskDetail> = {}): 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<void> {
|
||||
const { fetchTaskDetail } = await import("../../api");
|
||||
@@ -242,6 +242,12 @@ export async function resetTaskDetailFetchMock(): Promise<void> {
|
||||
vi.mocked(fetchTaskDetail).mockResolvedValue(makeTask());
|
||||
}
|
||||
|
||||
export async function resetTaskPromptFetchMock(): Promise<void> {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user