// @vitest-environment jsdom import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { flushSync } from "react-dom"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { SidebarServerInfo } from "./SidebarServerInfo"; const mockHealthApi = vi.hoisted(() => ({ get: vi.fn(), })); const mockInstanceSettingsApi = vi.hoisted(() => ({ getExperimental: vi.fn(), })); vi.mock("@/api/health", () => ({ healthApi: mockHealthApi, })); vi.mock("@/api/instanceSettings", () => ({ instanceSettingsApi: mockInstanceSettingsApi, })); async function flushReact() { for (let index = 0; index < 6; index += 1) { await Promise.resolve(); await new Promise((resolve) => window.setTimeout(resolve, 0)); } flushSync(() => {}); } async function flushReactMicrotasks() { for (let index = 0; index < 6; index += 1) { await Promise.resolve(); await vi.advanceTimersByTimeAsync(0); } flushSync(() => {}); } function mockEnabledSettings(enabled: boolean) { mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableServerInfoDebugView: enabled, }); } describe("SidebarServerInfo", () => { let container: HTMLDivElement; let root: Root | null = null; async function render() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); root = createRoot(container); flushSync(() => { root!.render( , ); }); await flushReact(); } beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); mockHealthApi.get.mockReset(); mockInstanceSettingsApi.getExperimental.mockReset(); }); afterEach(() => { flushSync(() => { root?.unmount(); }); root = null; container.remove(); document.body.replaceChildren(); vi.clearAllMocks(); vi.useRealTimers(); }); it("renders nothing while the experimental flag is disabled", async () => { mockEnabledSettings(false); await render(); expect(container.textContent).toBe(""); expect(mockHealthApi.get).not.toHaveBeenCalled(); }); it("shows restart and commit rows from health data when enabled", async () => { mockEnabledSettings(true); mockHealthApi.get.mockResolvedValue({ status: "ok", devServer: { enabled: true, restartRequired: false, reason: null, lastChangedAt: null, changedPathCount: 0, changedPathsSample: [], pendingMigrations: [], autoRestartEnabled: false, activeRunCount: 0, waitingForIdle: false, lastRestartAt: "2026-06-26T01:15:00.000Z", }, serverInfo: { processStartedAt: "2026-06-26T00:00:00.000Z", git: { available: true, fullSha: "abcdef1234567890abcdef1234567890abcdef12", shortSha: "abcdef1", branchName: "master", subject: "Add server info debug view", committedAt: "2026-06-25T23:00:00.000Z", localChanges: { available: true, hasLocalChanges: false, stagedFileCount: 0, unstagedFileCount: 0, untrackedFileCount: 0, }, }, }, }); await render(); expect(container.textContent).toContain("Last restarted"); expect(container.textContent).toContain("Running commit"); expect(container.textContent).toContain("Checkout state"); expect(container.querySelector('time[dateTime="2026-06-26T01:15:00.000Z"]')).not.toBeNull(); expect(container.textContent).toContain("abcdef1"); expect(container.textContent).toContain("Add server info debug view"); expect(container.textContent).toContain("Clean checkout"); }); it("polls health while the drawer is open and the dev server is active", async () => { vi.useFakeTimers(); mockEnabledSettings(true); mockHealthApi.get.mockResolvedValue({ status: "ok", devServer: { enabled: true, restartRequired: false, reason: null, lastChangedAt: null, changedPathCount: 0, changedPathsSample: [], pendingMigrations: [], autoRestartEnabled: false, activeRunCount: 0, waitingForIdle: false, lastRestartAt: "2026-06-26T01:15:00.000Z", }, serverInfo: { processStartedAt: "2026-06-26T00:00:00.000Z", git: { available: true, fullSha: "abcdef1234567890abcdef1234567890abcdef12", shortSha: "abcdef1", branchName: "master", subject: "Add server info debug view", committedAt: "2026-06-25T23:00:00.000Z", localChanges: { available: true, hasLocalChanges: false, stagedFileCount: 0, unstagedFileCount: 0, untrackedFileCount: 0, }, }, }, }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); root = createRoot(container); flushSync(() => { root!.render( , ); }); await flushReactMicrotasks(); expect(mockHealthApi.get).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(2000); await flushReactMicrotasks(); expect(mockHealthApi.get).toHaveBeenCalledTimes(2); }); it("shows path-free local change counts from health data", async () => { mockEnabledSettings(true); mockHealthApi.get.mockResolvedValue({ status: "ok", serverInfo: { processStartedAt: "2026-06-26T00:00:00.000Z", git: { available: true, fullSha: "abcdef1234567890abcdef1234567890abcdef12", shortSha: "abcdef1", branchName: "master", subject: "Add server info debug view", committedAt: "2026-06-25T23:00:00.000Z", localChanges: { available: true, hasLocalChanges: true, stagedFileCount: 3, unstagedFileCount: 2, untrackedFileCount: 1, }, }, }, }); await render(); expect(container.textContent).toContain("Local changes present (3 staged, 2 unstaged, 1 untracked)"); expect(container.textContent).not.toContain("server-info.ts"); }); it("refetches fresh health each time the drawer reopens, even within staleTime", async () => { mockEnabledSettings(true); const baseHealth = { status: "ok" as const, serverInfo: { processStartedAt: "2026-06-26T00:00:00.000Z", git: { available: true, fullSha: "1111111111111111111111111111111111111111", shortSha: "1111111", branchName: "master", subject: "First boot", committedAt: "2026-06-25T23:00:00.000Z", localChanges: { available: true, hasLocalChanges: false, stagedFileCount: 0, unstagedFileCount: 0, untrackedFileCount: 0, }, }, }, }; mockHealthApi.get.mockResolvedValue(baseHealth); // A long staleTime mirrors the production QueryClient: without // refetchOnMount "always", reopening the drawer would show cached data. const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: 30_000 } }, }); async function mountDrawer() { const drawerRoot = createRoot(container); flushSync(() => { drawerRoot.render( , ); }); await flushReact(); return drawerRoot; } const firstOpen = await mountDrawer(); expect(container.textContent).toContain("First boot"); flushSync(() => firstOpen.unmount()); // Server restarted: a fresh commit and process start should appear on reopen. mockHealthApi.get.mockResolvedValue({ ...baseHealth, serverInfo: { processStartedAt: "2026-06-26T02:00:00.000Z", git: { available: true, fullSha: "2222222222222222222222222222222222222222", shortSha: "2222222", branchName: "master", subject: "After restart", committedAt: "2026-06-26T01:30:00.000Z", localChanges: { available: true, hasLocalChanges: true, stagedFileCount: 1, unstagedFileCount: 0, untrackedFileCount: 0, }, }, }, }); const secondOpen = await mountDrawer(); expect(container.textContent).toContain("After restart"); expect(container.textContent).toContain("Local changes present (1 staged)"); expect(container.textContent).not.toContain("First boot"); flushSync(() => secondOpen.unmount()); }); it("falls back to process start and unavailable commit copy", async () => { mockEnabledSettings(true); mockHealthApi.get.mockResolvedValue({ status: "ok", serverInfo: { processStartedAt: "2026-06-26T00:00:00.000Z", git: { available: false, unavailableReason: "git_unavailable", }, }, }); await render(); expect(container.querySelector('time[dateTime="2026-06-26T00:00:00.000Z"]')).not.toBeNull(); expect(container.textContent).toContain("Commit unavailable"); }); });