import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { useSystemStore } from '@/stores/system-store'; /** * M8-9:use-shutdown-watcher 的 boot-id reload 行為測試。 * * 透過 retryServerHealth(匯出可測)間接覆蓋 handleBootIdCheck 路徑: * - 首次 fetch → setBootId,markOnline * - 相同 bootId → markOnline,不 reload * - 不同 bootId → 觸發 window.location.reload() * - 已觸發過 reload(sessionStorage guard)→ 同一 bootId 不再 reload */ vi.mock('@/lib/api', () => ({ getRelayHeaders: () => ({}), ensureRelayToken: vi.fn().mockResolvedValue(undefined), })); vi.mock('@/lib/constants', () => ({ getApiBaseUrl: () => 'http://localhost:3000/api', getWsBaseUrl: () => 'ws://localhost:3000', getRelayToken: () => 'test-token', })); import { retryServerHealth } from '@/hooks/use-shutdown-watcher'; const RELOAD_LOOP_GUARD_KEY = 'visiona-reload-loop-guard'; function mockFetchBootId(bootId: string) { return vi.fn().mockResolvedValue({ ok: true, json: async () => ({ success: true, data: { bootId, startedAt: Date.now() }, }), }); } describe('use-shutdown-watcher — boot-id reload (M8-9)', () => { const reloadSpy = vi.fn(); beforeEach(() => { useSystemStore.setState({ serverOnline: true, forcedOffline: false, offlineReason: null, bootId: null, consecutiveFailures: 0, }); window.sessionStorage.clear(); reloadSpy.mockClear(); // 替換 window.location.reload 為 spy(jsdom 的 location 非 configurable, // 用 defineProperty 套一層替代物件)。 Object.defineProperty(window, 'location', { configurable: true, value: { ...window.location, reload: reloadSpy }, }); }); afterEach(() => { vi.restoreAllMocks(); }); it('first fetch should persist bootId and mark online without reload', async () => { const fetchMock = mockFetchBootId('boot-1'); vi.stubGlobal('fetch', fetchMock); const ok = await retryServerHealth(); expect(ok).toBe(true); expect(useSystemStore.getState().bootId).toBe('boot-1'); expect(useSystemStore.getState().serverOnline).toBe(true); expect(reloadSpy).not.toHaveBeenCalled(); }); it('second fetch with same bootId should just mark online', async () => { useSystemStore.setState({ bootId: 'boot-1' }); const fetchMock = mockFetchBootId('boot-1'); vi.stubGlobal('fetch', fetchMock); const ok = await retryServerHealth(); expect(ok).toBe(true); expect(useSystemStore.getState().bootId).toBe('boot-1'); expect(reloadSpy).not.toHaveBeenCalled(); }); it('fetch with different bootId should trigger window.location.reload', async () => { useSystemStore.setState({ bootId: 'boot-1' }); const fetchMock = mockFetchBootId('boot-2'); vi.stubGlobal('fetch', fetchMock); const ok = await retryServerHealth(); expect(ok).toBe(true); expect(reloadSpy).toHaveBeenCalledTimes(1); // mismatch 不該更新 store 的 bootId(等 reload 後重走 first 路徑) expect(useSystemStore.getState().bootId).toBe('boot-1'); // guard 應被設為新的 bootId expect(window.sessionStorage.getItem(RELOAD_LOOP_GUARD_KEY)).toBe('boot-2'); }); it('should not reload twice for the same new bootId (loop guard)', async () => { useSystemStore.setState({ bootId: 'boot-1' }); window.sessionStorage.setItem(RELOAD_LOOP_GUARD_KEY, 'boot-2'); const fetchMock = mockFetchBootId('boot-2'); vi.stubGlobal('fetch', fetchMock); const ok = await retryServerHealth(); expect(ok).toBe(true); expect(reloadSpy).not.toHaveBeenCalled(); // guard 命中時應該把 store 對齊新 bootId 並 markOnline,避免卡在假離線 expect(useSystemStore.getState().bootId).toBe('boot-2'); expect(useSystemStore.getState().serverOnline).toBe(true); }); it('fetch failure should record failure and not reload', async () => { useSystemStore.setState({ bootId: 'boot-1' }); const fetchMock = vi.fn().mockResolvedValue({ ok: false }); vi.stubGlobal('fetch', fetchMock); const ok = await retryServerHealth(); expect(ok).toBe(false); expect(reloadSpy).not.toHaveBeenCalled(); expect(useSystemStore.getState().consecutiveFailures).toBe(1); }); });