import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { MigrationWorkspace } from "./MigrationWorkspace"; import { clearOperatorCredential, setOperatorCredential } from "../lib/api"; const activePlan = (overrides: Record = {}) => ({ id: "plan-1", project_id: "project-1", project_binding_id: "binding-1", capability_contract_id: "contract-1", migration_class: "REQUIRES_REINDEX", environment: "LAB", state: "BACKFILL_PAUSED", version: 7, generation: 1, adapter: { key: "examplerag.qdrant-reindex", version: "1", fingerprint: "a".repeat(64), operations: [] }, source_identity: {}, target_identity: {}, source_data_target: "rag_source_v1", target_shadow_target: "rag_shadow_v2", source_space_ref: "space-v1", target_space_id: "space-2", corpus_revision: "corpus-1", migration_policy_revision: "m13-1", validation_policy_revision_id: "policy-1", lifecycle_approval_id: "approval-1", rollback_target_ref: "rag_source_v1", total_expected_items: 597, completed_items: 256, failed_items: 0, retryable_items: 0, permanent_failed_items: 0, batch_size: 32, priority: "BACKGROUND", rollback_retention_days: 30, plan_fingerprint: "b".repeat(64), approval_fingerprint: "c".repeat(64), failure_details: {}, last_cursor: "256", cancel_requested: false, immutable_at: "2026-08-26T10:00:00Z", created_at: "2026-08-26T10:00:00Z", updated_at: "2026-08-26T10:01:00Z", ...overrides, }); const validation = (planId = "plan-1", overrides: Record = {}) => ({ id: `validation-${planId}`, migration_plan_id: planId, generation: 1, expected_count: 597, actual_count: 597, missing_count: 0, duplicate_count: 0, malformed_count: 0, non_finite_count: 0, wrong_dimension_count: 0, content_hash_mismatch_count: 0, wrong_space_count: 0, comparable: true, critical_regressions: 0, passed: true, technical_cutover_eligible: true, project_promotion_eligible: false, blockers: ["EXTERNAL_VALIDATION_NOT_SATISFIED"], target_fingerprint: "d".repeat(64), snapshot_fingerprint: "e".repeat(64), evidence: {}, created_at: "2026-08-26T10:02:00Z", ...overrides, }); const cutover = (planId = "plan-1", overrides: Record = {}) => ({ id: `cutover-${planId}`, migration_plan_id: planId, stage: "ROLLED_BACK", generation: 1, source_before: "rag_source_v1", target_after: "rag_shadow_v2", external_state_fingerprint: "f".repeat(64), health_evidence: {}, failure_details: {}, switch_duration_ms: 3.2, rollback_duration_ms: 2.1, started_at: "2026-08-26T10:03:00Z", finished_at: "2026-08-26T10:04:00Z", ...overrides, }); const auditEvent = { id: "event-1", migration_plan_id: "plan-1", event_type: "BACKFILL_PAUSED", before_state: "BACKFILLING", after_state: "BACKFILL_PAUSED", actor: "operator", reason: "interruption rehearsal", policy_revision: "m13-1", evidence_refs: [], source_identity: {}, target_identity: {}, generation: 1, change_id: "change-1", details: {}, occurred_at: "2026-08-26T10:01:00Z", }; function mockMigration(options: { plans?: Record[]; validations?: Record[]; cutovers?: Record[]; events?: Record[]; } = {}) { const plans = options.plans ?? [activePlan()]; const validations = options.validations ?? [validation()]; const cutovers = options.cutovers ?? [cutover()]; const events = options.events ?? [auditEvent]; const requests: { url: string; method: string }[] = []; const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); requests.push({ url, method: init?.method ?? "GET" }); let payload: unknown = []; if (url.endsWith("/plans")) payload = plans; if (url.includes("/validation")) { const planId = decodeURIComponent(url.split("/plans/")[1]?.split("/validation")[0] ?? ""); payload = validations.filter((item) => item.migration_plan_id === planId); } if (url.includes("/cutovers")) payload = cutovers; if (url.includes("/events")) payload = events; if (url.includes("/backfill/start")) payload = plans.find((item) => item.id === "plan-1") ?? activePlan(); return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 })); }); vi.stubGlobal("fetch", fetchMock); return { fetchMock, requests }; } afterEach(() => { clearOperatorCredential(); cleanup(); vi.unstubAllGlobals(); }); describe("M13 migration operator workspace", () => { it("separates current work from terminal history and counts only current evidence", async () => { mockMigration({ plans: [activePlan(), activePlan({ id: "plan-old", state: "ROLLED_BACK", source_data_target: "archive_source", target_shadow_target: "archive_target", total_expected_items: 100, completed_items: 100, rollback_target_ref: "archive_source", }), activePlan({ id: "plan-committed", state: "CUTOVER_COMMITTED", source_data_target: "committed_source", target_shadow_target: "committed_target", total_expected_items: 10, completed_items: 10, rollback_target_ref: "committed_source", })], validations: [validation(), validation("plan-old", { project_promotion_eligible: true })], cutovers: [cutover(), cutover("plan-committed", { stage: "COMMITTED", source_before: "committed_source", target_after: "committed_target", rollback_duration_ms: null, })], }); setOperatorCredential("operator-test"); render(); await screen.findByText("266 / 607"); expect(screen.getByText("Current item progress").closest(".migration-fact")).toHaveTextContent("266 / 607"); expect(screen.getByText("Current migrations").closest(".migration-fact")).toHaveTextContent("2"); expect(screen.getByText("Current project-promotion eligible").closest(".migration-fact")).toHaveTextContent("0"); fireEvent.click(screen.getByRole("tab", { name: "plans" })); expect(await screen.findByRole("heading", { name: "Current migrations (2)" })).toBeInTheDocument(); expect(screen.getByText("Historical migrations (1)")).toBeInTheDocument(); const current = screen.getByRole("heading", { name: "rag_source_v1 → rag_shadow_v2" }).closest("article"); const history = screen.getByText("archive_source → archive_target").closest("article"); const committed = screen.getByRole("heading", { name: "committed_source → committed_target" }).closest("article"); expect(current).toHaveTextContent("Current migration"); expect(current).toHaveTextContent("Validation snapshot"); expect(history).toHaveTextContent("Historical record"); expect(history).toHaveTextContent("Read-only history"); expect(history).not.toHaveTextContent("Start/resume"); expect(history).not.toHaveTextContent("Cancel"); expect(committed).toHaveTextContent("Current migration"); expect(committed).toHaveTextContent("Rollback targetcommitted_source"); expect(committed).toHaveTextContent("30-day retention policy"); expect(committed).not.toHaveTextContent("Cancel"); }); it("keeps a selected historical plan visible and read-only for Quick Jump", async () => { mockMigration({ plans: [activePlan({ id: "plan-old", state: "ROLLED_BACK" })] }); setOperatorCredential("operator-test"); render(); const target = await screen.findByText("QUICK JUMP TARGET"); expect(screen.getByRole("tab", { name: "plans" })).toHaveAttribute("aria-selected", "true"); expect(target.closest("article")).toHaveTextContent("Historical record"); expect(target.closest("article")).toHaveTextContent("rag_source_v1 → rag_shadow_v2"); expect(target.closest("article")).not.toHaveTextContent("Start/resume"); }); it("surfaces missing validation and cutover evidence on plans requiring reconciliation", async () => { mockMigration({ plans: [activePlan({ state: "MANUAL_INTERVENTION_REQUIRED" })], validations: [], cutovers: [], }); setOperatorCredential("operator-test"); render(); fireEvent.click(screen.getByRole("tab", { name: "plans" })); const card = (await screen.findByText("rag_source_v1 → rag_shadow_v2")).closest("article"); expect(card).toHaveTextContent("Reconciliation required"); expect(card).toHaveTextContent("Validation evidence not recorded"); expect(card).toHaveTextContent("Cutover journal not recorded"); expect(card).toHaveTextContent(/Rollback target\s*rag_source_v1/); expect(card).not.toHaveTextContent("Start/resume"); expect(card).not.toHaveTextContent("Pause"); expect(card).not.toHaveTextContent("Cancel"); }); it("does not present prior-generation evidence as current reconciliation context", async () => { mockMigration({ plans: [activePlan({ generation: 2, state: "MANUAL_INTERVENTION_REQUIRED" })], validations: [validation("plan-1", { generation: 1, project_promotion_eligible: true })], cutovers: [cutover("plan-1", { generation: 1, stage: "COMMITTED" })], }); setOperatorCredential("operator-test"); render(); fireEvent.click(screen.getByRole("tab", { name: "plans" })); const card = (await screen.findByRole("heading", { name: "rag_source_v1 → rag_shadow_v2" })).closest("article"); expect(card).toHaveTextContent("stale generation"); expect(card).toHaveTextContent("current plan generation is 2"); expect(card).toHaveTextContent("Latest cutover journal is from generation 1"); expect(card).toHaveTextContent("not proven pending reconciliation"); expect(card).toHaveTextContent("not currently eligible"); expect(card).not.toHaveTextContent("snapshot eligible"); expect(screen.getByRole("heading", { name: "Needs operator attention (1)" })).toBeInTheDocument(); }); it("waits for an explicit current-plan action and never mutates history", async () => { const { requests } = mockMigration({ plans: [activePlan(), activePlan({ id: "plan-old", state: "ROLLED_BACK" })], }); setOperatorCredential("operator-test"); render(); await screen.findByText("256 / 597"); expect(requests.every((request) => request.method === "GET")).toBe(true); fireEvent.click(screen.getByRole("tab", { name: "plans" })); expect(await screen.findByRole("heading", { name: "Current migrations (1)" })).toBeInTheDocument(); expect(requests.every((request) => request.method === "GET")).toBe(true); fireEvent.click(screen.getByRole("button", { name: "Start/resume" })); await screen.findByText("Background backfill explicitly started or resumed."); await waitFor(() => expect(requests.some((request) => request.method === "POST" && request.url.endsWith("/plan-1/backfill/start"))).toBe(true)); expect(requests.some((request) => request.method !== "GET" && request.url.includes("plan-old"))).toBe(false); }); it("separates technical cutover from project-promotion eligibility", async () => { mockMigration(); render(); fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "operator-test" } }); fireEvent.click(screen.getByRole("button", { name: "Load migrations" })); await screen.findByText("256 / 597"); fireEvent.click(screen.getByRole("tab", { name: "validation" })); expect(screen.getByText("technical pass")).toBeInTheDocument(); expect(screen.getByText("external validation not satisfied")).toBeInTheDocument(); expect(screen.getByText("blocked")).toBeInTheDocument(); }); it("shows resumability, exact cutover identities, rollback timing and audit history", async () => { mockMigration(); render(); fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "operator-test" } }); fireEvent.click(screen.getByRole("button", { name: "Load migrations" })); await screen.findByText("256 / 597"); fireEvent.click(screen.getByRole("tab", { name: "plans" })); expect(screen.getByRole("button", { name: "Start/resume" })).toBeInTheDocument(); fireEvent.click(screen.getByRole("tab", { name: "cutover" })); expect(screen.getByText("3.2 ms")).toBeInTheDocument(); expect(screen.getByText("2.1 ms")).toBeInTheDocument(); fireEvent.click(screen.getByRole("tab", { name: "history" })); expect(screen.getByText("interruption rehearsal")).toBeInTheDocument(); }); it("does not select another plan when a Quick Jump target is absent", async () => { mockMigration(); setOperatorCredential("operator-test"); render(); expect(await screen.findByRole("status")).toHaveTextContent(/selected migration plan is not in the loaded index/i); expect(screen.queryByText("QUICK JUMP TARGET")).not.toBeInTheDocument(); }); });