Place low-frequency spec alignment provenance after plan and relationship content.\n\n- Move the Definition-tab spec-lock report to the final section.\n- Cover populated, unavailable, and absent spec-lock report states.\n- Document the placement and add a patch changeset.\n\nFiles changed:\n .changeset/fn-9095-spec-alignment-position.md | 7 ++\n docs/dashboard-guide.md | 2 +-\n .../dashboard/app/components/TaskDetailModal.tsx | 87 +++++++++++-----------\n .../__tests__/TaskDetailModal.spec-lock.test.tsx | 63 ++++++++++++++++\n 4 files changed, 115 insertions(+), 44 deletions(-) Fusion-Task-Id: FN-9095 Fusion-Task-Lineage: c017325c-b7c1-4f0e-abab-49462903a778 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
144 lines
5.1 KiB
TypeScript
144 lines
5.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import {
|
|
makeTask,
|
|
noop,
|
|
noopDelete,
|
|
noopMerge,
|
|
noopMove,
|
|
noopOpenDetail,
|
|
setupTaskDetailModalHooks,
|
|
} from "./TaskDetailModal.test-helpers";
|
|
import { TaskDetailContent } from "../TaskDetailModal";
|
|
|
|
setupTaskDetailModalHooks();
|
|
|
|
afterEach(async () => {
|
|
const { fetchSpecLock } = await import("../../api");
|
|
vi.mocked(fetchSpecLock).mockReset();
|
|
vi.mocked(fetchSpecLock).mockResolvedValue({ latestLock: null, activeLock: null, currentPlan: null, report: null, latestReport: null, history: { locks: [], currentPlans: [], reports: [] } });
|
|
});
|
|
|
|
describe("TaskDetailModal spec-lock report", () => {
|
|
it("renders persisted Mission-statement divergence and immutable provenance in the shared Definition surface", async () => {
|
|
const { fetchSpecLock } = await import("../../api");
|
|
vi.mocked(fetchSpecLock).mockResolvedValue({
|
|
latestLock: {
|
|
version: 1,
|
|
acceptedAt: "2026-08-09T19:34:00.000Z",
|
|
approvalFingerprint: "approved-hash",
|
|
currentPlanVersion: 1,
|
|
currentPlanHash: "plan-hash",
|
|
plan: {} as never,
|
|
},
|
|
activeLock: null,
|
|
currentPlan: {
|
|
version: 2,
|
|
sourceRevision: 42,
|
|
sourceHash: "source-hash",
|
|
capturedAt: "2026-08-09T19:35:00.000Z",
|
|
plan: {} as never,
|
|
},
|
|
report: {
|
|
lockVersion: 1,
|
|
currentPlanVersion: 2,
|
|
currentPlanHash: "changed-plan-hash",
|
|
status: "available",
|
|
findings: [{ kind: "plan-deviation", category: "mission-statement", priorHash: "prior", currentHash: "current" }],
|
|
alignment: "diverged-needs-review",
|
|
executionHash: "execution",
|
|
reportHash: "report",
|
|
},
|
|
latestReport: null,
|
|
history: { locks: [{ version: 1 }], currentPlans: [{ version: 1 }, { version: 2 }], reports: [{}, {}] },
|
|
} as never);
|
|
|
|
render(
|
|
<TaskDetailContent
|
|
initialTab="definition"
|
|
active
|
|
task={makeTask({ prompt: "## Mission\n\nChanged" })}
|
|
onRequestClose={noop}
|
|
onMoveTask={noopMove}
|
|
onDeleteTask={noopDelete}
|
|
onMergeTask={noopMerge}
|
|
onOpenDetail={noopOpenDetail}
|
|
addToast={vi.fn()}
|
|
onTaskUpdated={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const report = await screen.findByTestId("spec-lock-report");
|
|
expect(report).toHaveAccessibleName("Spec lock alignment");
|
|
expect(report).toHaveTextContent("diverged-needs-review");
|
|
expect(report).toHaveTextContent("plan-deviation: mission-statement");
|
|
expect(report).toHaveTextContent("source revision 42");
|
|
expect(report).toHaveTextContent("source hash source-hash");
|
|
expect(report).toHaveTextContent("Retained history: lock v1; plan v1, plan v2; 2 reports");
|
|
for (const heading of ["Progress", "Original prompt", "Dependencies", "Blocking"]) {
|
|
const anchor = screen.getByText(heading, { exact: true });
|
|
expect(anchor.compareDocumentPosition(report) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
}
|
|
await waitFor(() => expect(fetchSpecLock).toHaveBeenCalledWith("FN-099", undefined));
|
|
});
|
|
|
|
it("keeps an unavailable all-null report at the end without rendering findings", async () => {
|
|
const { fetchSpecLock } = await import("../../api");
|
|
vi.mocked(fetchSpecLock).mockResolvedValue({
|
|
latestLock: null,
|
|
activeLock: null,
|
|
currentPlan: null,
|
|
report: null,
|
|
latestReport: null,
|
|
history: { locks: [], currentPlans: [], reports: [] },
|
|
});
|
|
|
|
render(
|
|
<TaskDetailContent
|
|
initialTab="definition"
|
|
active
|
|
task={makeTask()}
|
|
onRequestClose={noop}
|
|
onMoveTask={noopMove}
|
|
onDeleteTask={noopDelete}
|
|
onMergeTask={noopMerge}
|
|
onOpenDetail={noopOpenDetail}
|
|
addToast={vi.fn()}
|
|
onTaskUpdated={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const report = await screen.findByTestId("spec-lock-report");
|
|
expect(report).toHaveTextContent("unavailable");
|
|
expect(report).toHaveTextContent("v—");
|
|
expect(report.querySelector(".spec-lock-findings")).toBeNull();
|
|
const blocking = screen.getByText("Blocking", { exact: true });
|
|
expect(blocking.compareDocumentPosition(report) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
});
|
|
|
|
it("does not leave a spec-lock shell when no spec-lock data exists", async () => {
|
|
const { fetchSpecLock } = await import("../../api");
|
|
vi.mocked(fetchSpecLock).mockResolvedValue(null as never);
|
|
|
|
render(
|
|
<TaskDetailContent
|
|
initialTab="definition"
|
|
active
|
|
task={makeTask()}
|
|
onRequestClose={noop}
|
|
onMoveTask={noopMove}
|
|
onDeleteTask={noopDelete}
|
|
onMergeTask={noopMerge}
|
|
onOpenDetail={noopOpenDetail}
|
|
addToast={vi.fn()}
|
|
onTaskUpdated={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await waitFor(() => expect(fetchSpecLock).toHaveBeenCalledWith("FN-099", undefined));
|
|
expect(screen.queryByTestId("spec-lock-report")).toBeNull();
|
|
expect(screen.getByText("Dependencies", { exact: true })).toBeInTheDocument();
|
|
expect(screen.getByText("Blocking", { exact: true })).toBeInTheDocument();
|
|
});
|
|
});
|