Files
fusion/packages/dashboard/app/components/__tests__/TaskDetailModal.custom-fields.test.tsx
gsxdsm 3ebaa321f8 fix(review): apply code-review fixes — runId/foreachNodeId wiring, worktree-leak cleanup, rework re-execution, instance pruning, SIGKILL fallback, type dedup, board memo split, agent-native field-schema context + fn_workflow_get
17 findings from 12-reviewer code review applied:
- P1 runId trio (pin-probe/resume/markIntegrated used placeholder runId; 4-reviewer corroboration) + production-wiring tests
- P1 worktree/branch release on instance failure/exhaustion/abort
- P2 runGraphTaskStep no longer masks step-session failures; rejected memo cleared so rework re-executes
- P2 clearStaleInstanceStates wired at run start/end (mirrors branch pruning)
- P2 code-node timeout killSignal SIGKILL; dead template-recursion removed
- P1/P2 field-type re-declarations replaced with @fusion/core imports (stale comments removed)
- P2 Board memo split + TaskCard comparator stringify guard + modal prop-driven field defs
- HIGH agent-native: executor prompt injects custom-field schema/values; self-correcting rejection text; fn_workflow_get; fn_task_update bare-call guard; integration-conflict task log

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:01:55 -07:00

119 lines
4.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import type { WorkflowFieldDefinition } from "../../api";
import {
makeTask,
noop,
noopDelete,
noopMerge,
noopMove,
noopOpenDetail,
setupTaskDetailModalHooks,
} from "./TaskDetailModal.test-helpers";
import { TaskDetailModal } from "../TaskDetailModal";
import * as dashboardApi from "../../api";
import { FileBrowserProvider } from "../../context/FileBrowserContext";
setupTaskDetailModalHooks();
function renderModal(task = makeTask({ column: "done" })) {
return render(
<FileBrowserProvider openFile={vi.fn()}>
<TaskDetailModal
task={task}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>
</FileBrowserProvider>,
);
}
describe("TaskDetailModal custom fields (U13/KTD-14)", () => {
beforeEach(() => vi.clearAllMocks());
it("renders no fields section when the workflow declares no fields (today's UI)", async () => {
vi.spyOn(dashboardApi, "fetchBoardWorkflows").mockResolvedValue({
flagEnabled: true,
defaultWorkflowId: "builtin:coding",
workflows: [{ id: "builtin:coding", name: "Coding", columns: [] }],
taskWorkflowIds: {},
});
renderModal();
// Allow the field-defs fetch to settle.
await waitFor(() => expect(dashboardApi.fetchBoardWorkflows).toHaveBeenCalled());
expect(screen.queryByTestId("task-fields-section")).toBeNull();
});
it("renders the schema-driven fields section when the workflow declares fields", async () => {
vi.spyOn(dashboardApi, "fetchBoardWorkflows").mockResolvedValue({
flagEnabled: true,
defaultWorkflowId: "builtin:coding",
workflows: [
{
id: "builtin:coding",
name: "Coding",
columns: [],
fields: [
{ id: "owner", name: "Owner", type: "string", render: { placement: "detail" } },
],
},
],
taskWorkflowIds: { "FN-001": "builtin:coding" },
});
renderModal(makeTask({ id: "FN-001", column: "done", customFields: { owner: "alice" } }));
await waitFor(() => expect(screen.getByTestId("task-fields-section")).toBeTruthy());
expect((screen.getByLabelText("Owner") as HTMLInputElement).value).toBe("alice");
});
it("uses workflowFieldDefs prop directly and skips the board-workflows fetch", async () => {
const fetchSpy = vi.spyOn(dashboardApi, "fetchBoardWorkflows");
const defs: WorkflowFieldDefinition[] = [
{ id: "owner", name: "Owner", type: "string", render: { placement: "detail" } },
];
render(
<FileBrowserProvider openFile={vi.fn()}>
<TaskDetailModal
task={makeTask({ id: "FN-002", column: "done", customFields: { owner: "bob" } })}
workflowFieldDefs={defs}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>
</FileBrowserProvider>,
);
await waitFor(() => expect(screen.getByTestId("task-fields-section")).toBeTruthy());
expect((screen.getByLabelText("Owner") as HTMLInputElement).value).toBe("bob");
// The fetch must NOT have been triggered since the prop was provided.
expect(fetchSpy).not.toHaveBeenCalled();
});
it("renders no fields section when workflowFieldDefs prop is an empty array", async () => {
const fetchSpy = vi.spyOn(dashboardApi, "fetchBoardWorkflows");
render(
<FileBrowserProvider openFile={vi.fn()}>
<TaskDetailModal
task={makeTask({ id: "FN-003", column: "done" })}
workflowFieldDefs={[]}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>
</FileBrowserProvider>,
);
// Give React a tick to settle; no section should appear.
await new Promise((r) => setTimeout(r, 50));
expect(screen.queryByTestId("task-fields-section")).toBeNull();
expect(fetchSpy).not.toHaveBeenCalled();
});
});