import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { MigrationPopup } from "./MigrationPopup"; const { getMock, postMock, getMigration, setMigration } = vi.hoisted(() => ({ getMock: vi.fn(), postMock: vi.fn(), getMigration: vi.fn(), setMigration: vi.fn(), })); vi.mock("../lib/api-client", () => ({ apiClient: { GET: getMock, POST: postMock }, apiErrorMessage: (e: unknown, fb = "Request failed") => e instanceof Error ? e.message : ((e as { message?: string })?.message ?? fb), })); vi.mock("../lib/bridge", () => ({ aoBridge: { appState: { getMigration, setMigration } } })); function renderPopup() { const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( , ); return qc; } beforeEach(() => { getMock.mockReset(); postMock.mockReset(); getMigration.mockReset(); setMigration.mockReset(); getMigration.mockResolvedValue({ status: "pending" }); getMock.mockResolvedValue({ data: { available: true, legacyRoot: "/home/u/.agent-orchestrator" }, error: undefined }); postMock.mockResolvedValue({ data: { report: { projectsImported: 2, projectsSkipped: 1 } }, error: undefined }); setMigration.mockResolvedValue(undefined); }); describe("MigrationPopup", () => { it("shows when a legacy install is available and the marker is pending", async () => { renderPopup(); expect(await screen.findByText(/Import projects from your earlier AO/i)).toBeInTheDocument(); expect(screen.getByText("/home/u/.agent-orchestrator")).toBeInTheDocument(); }); it("renders nothing when the marker is declined", async () => { getMigration.mockResolvedValue({ status: "declined" }); renderPopup(); await waitFor(() => expect(getMigration).toHaveBeenCalled()); expect(screen.queryByText(/Import projects from your earlier AO/i)).not.toBeInTheDocument(); expect(getMock).not.toHaveBeenCalled(); }); it("Proceed imports, marks completed, and retires", async () => { renderPopup(); await screen.findByText(/Import projects from your earlier AO/i); await userEvent.click(screen.getByRole("button", { name: "Proceed" })); await waitFor(() => expect(postMock).toHaveBeenCalledWith("/api/v1/import")); expect(setMigration).toHaveBeenCalledWith(expect.objectContaining({ status: "completed" })); await waitFor(() => expect(screen.queryByText(/Import projects from your earlier AO/i)).not.toBeInTheDocument()); }); it("Don't Migrate records declined", async () => { renderPopup(); await screen.findByText(/Import projects from your earlier AO/i); await userEvent.click(screen.getByRole("button", { name: "Don't Migrate" })); expect(setMigration).toHaveBeenCalledWith(expect.objectContaining({ status: "declined" })); }); it("Skip dismisses without writing the marker", async () => { renderPopup(); await screen.findByText(/Import projects from your earlier AO/i); await userEvent.click(screen.getByRole("button", { name: "Skip" })); expect(setMigration).not.toHaveBeenCalled(); expect(screen.queryByText(/Import projects from your earlier AO/i)).not.toBeInTheDocument(); }); it("a failed import shows the lossless reassurance and marks failed", async () => { postMock.mockResolvedValue({ data: undefined, error: { message: "disk full" } }); renderPopup(); await screen.findByText(/Import projects from your earlier AO/i); await userEvent.click(screen.getByRole("button", { name: "Proceed" })); expect(await screen.findByText(/nothing is ever deleted/i)).toBeInTheDocument(); expect(setMigration).toHaveBeenCalledWith(expect.objectContaining({ status: "failed", error: "disk full" })); }); });