// @vitest-environment jsdom /** * T-176/T-177/T-178 — the three org-admin HEALTH tabs (jobs / search / * notifications): render + API wiring + the audited requeue mutation. * * Real react-query (QueryClientProvider) so the wiring is exercised; the API * client + toast are mocked. Asserts the cluster-scope labelling, the no-leak * org-scoping surface, the "search down → never a spinner" state, and that * Requeue → orgAdminApi.jobsRequeue with the right queue + jobId. */ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; import { act, type ReactNode } from "react"; import { createRoot, type Root } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ toast: vi.fn(), jobsOverview: vi.fn(async () => ({ scope: "cluster" as const, queues: [ { name: "email", waiting: 0, active: 0, completed: 0, failed: 1 }, { name: "files", waiting: 2, active: 1, completed: 3, failed: 0 } ] })), jobsFailed: vi.fn(async () => ({ scope: "cluster" as const, jobs: [ { queue: "email", jobId: "j-1", name: "email-notification", payload: "{\"a\":1}", failedReason: "boom", failedAt: "2026-01-01T00:00:00.000Z", attemptsMade: 1 } ] })), jobsRequeue: vi.fn(async () => ({ queued: true as const, queue: "email" as const, jobId: "j-1" })), // `Promise` so the "down" override (error: string, docCount: null) is // assignable — the component is typed against the REAL module, not this mock. searchHealth: vi.fn(async (): Promise => ({ healthy: true, error: null, workspaces: 1, sourceCount: 2, docCount: 2, drift: 0, lagMs: 500, lastIndexedAt: "2026-01-01T00:00:00.000Z", lastReconcileAt: null })), searchReindex: vi.fn(async () => ({ jobId: "ri-1", status: "processing" as const, workspaceId: null })), notificationsHealth: vi.fn(async () => ({ workspaces: 1, inAppTotal: 5, inAppUnread: 2, emailQueued: 3, emailFailed: 1, failedJobs: [ { notificationId: "notif-1", jobId: "j-1", name: "email-notification", failedReason: "boom", failedAt: "2026-01-01T00:00:00.000Z", attemptsMade: 1 } ] })) })); vi.mock("@/lib/org-admin", () => ({ orgAdminApi: { jobsOverview: mocks.jobsOverview, jobsFailed: mocks.jobsFailed, jobsRequeue: mocks.jobsRequeue, searchHealth: mocks.searchHealth, searchReindex: mocks.searchReindex, notificationsHealth: mocks.notificationsHealth } })); vi.mock("@/stores/ui-store", () => ({ toast: mocks.toast })); import { AdminJobsTab } from "../JobsTab"; import { AdminSearchHealthTab } from "../SearchHealthTab"; import { AdminNotificationsTab } from "../NotificationsTab"; let root: Root; let host: HTMLElement; let client: QueryClient; function q(sel: string): Element | null { return host.querySelector(sel); } function qs(sel: string): string | null { return host.querySelector(sel)?.textContent ?? null; } async function render(node: ReactNode) { client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }); root.render( {node} ); } /** Poll until `predicate` holds (the house pattern — a fixed flush is racy). */ async function waitUntil(predicate: () => boolean, budgetMs = 2000) { const started = Date.now(); while (!predicate()) { if (Date.now() - started > budgetMs) return; await act(async () => { await new Promise((r) => setTimeout(r, 10)); }); } } beforeEach(() => { document.body.innerHTML = ""; host = document.createElement("div"); document.body.appendChild(host); root = createRoot(host); vi.clearAllMocks(); }); afterEach(async () => { await act(async () => root.unmount()); host.remove(); }); describe("AdminJobsTab (T-176)", () => { it("renders the cluster-scope queue counts + the failed job with a Requeue action", async () => { await render(); await waitUntil(() => Boolean(q('[data-testid="jobs-queue-email"]'))); // The cluster scope is surfaced in the UI copy (never pretend org-scoped). expect(host.textContent).toContain("cluster-wide"); expect(q('[data-testid="jobs-queue-email"]')).toBeTruthy(); expect(q('[data-testid="jobs-queue-files"]')).toBeTruthy(); expect(qs('[data-testid="jobs-queue-files"]')).toContain("2"); // waiting // The failed job row + its Requeue control. expect(q('[data-testid="jobs-requeue-j-1"]')).toBeTruthy(); }); it("Requeue calls orgAdminApi.jobsRequeue with the job's queue + jobId", async () => { await render(); await waitUntil(() => Boolean(q('[data-testid="jobs-requeue-j-1"]'))); await act(async () => { (q('[data-testid="jobs-requeue-j-1"]') as HTMLButtonElement).click(); await new Promise((r) => setTimeout(r, 30)); }); expect(mocks.jobsRequeue).toHaveBeenCalledWith("org1", { queue: "email", jobId: "j-1" }); expect(mocks.toast).toHaveBeenCalledWith("success", "Job requeued"); }); }); describe("AdminSearchHealthTab (T-177)", () => { it("renders the healthy state + source/doc counts and a reindex trigger", async () => { await render(); await waitUntil(() => Boolean(q('[data-testid="search-health-ok"]'))); expect(q('[data-testid="search-health-ok"]')).toBeTruthy(); expect(q('[data-testid="search-health-down"]')).toBeNull(); expect(host.textContent).toContain("Source docs"); expect(host.textContent).toContain("Index docs"); expect(q('[data-testid="search-health-reindex"]')).toBeTruthy(); }); it("Reindex enqueues an org-scoped reindex", async () => { await render(); await waitUntil(() => Boolean(q('[data-testid="search-health-reindex"]'))); await act(async () => { (q('[data-testid="search-health-reindex"]') as HTMLButtonElement).click(); await new Promise((r) => setTimeout(r, 30)); }); expect(mocks.searchReindex).toHaveBeenCalledWith("org1"); }); it("search down → a 'unavailable' state (never a spinner loop)", async () => { mocks.searchHealth.mockResolvedValueOnce({ healthy: false, error: "SEARCH_UNAVAILABLE", workspaces: 1, sourceCount: 2, docCount: null, drift: null, lagMs: null, lastIndexedAt: null, lastReconcileAt: null }); await render(); await waitUntil(() => Boolean(q('[data-testid="search-health-down"]'))); expect(q('[data-testid="search-health-down"]')).toBeTruthy(); expect(host.textContent).toContain("SEARCH_UNAVAILABLE"); }); }); describe("AdminNotificationsTab (T-178)", () => { it("renders in-app volume + email queued/failed + the org-scoped failed jobs", async () => { await render(); await waitUntil(() => (host.textContent ?? "").includes("In-app total")); expect(host.textContent).toContain("In-app total"); expect(host.textContent).toContain("Email queued (cluster)"); expect(host.textContent).toContain("Email failed (org)"); // The org-scoped failed job surfaces its notification + reason. expect(host.textContent).toContain("notif-1"); expect(host.textContent).toContain("boom"); }); });