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>
119 lines
4.2 KiB
TypeScript
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();
|
|
});
|
|
});
|