import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' const enabled = vi.fn() vi.mock('../api', () => ({ pluginsApi: { enabled: (...args) => enabled(...args) } })) const { loadEnabledPlugins, isPluginEnabled, resetEnabledPlugins } = await import('./enabledPlugins') beforeEach(() => { resetEnabledPlugins() enabled.mockReset() vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('loadEnabledPlugins', () => { it('resolves the enabled set and gates on it', async () => { enabled.mockResolvedValue({ data: { data: ['machines', 'printers'] } }) await loadEnabledPlugins() expect(isPluginEnabled('machines')).toBe(true) expect(isPluginEnabled('usb')).toBe(false) }) it('fails open on error', async () => { enabled.mockRejectedValue(new Error('boom')) await loadEnabledPlugins() expect(isPluginEnabled('usb')).toBe(true) }) it('does not hang the router when the request never settles', async () => { // What an app-pool restart looks like: IIS queues the request while the // worker starts, and axios has no timeout. enabled.mockReturnValue(new Promise(() => {})) const pending = loadEnabledPlugins() await vi.advanceTimersByTimeAsync(5000) await expect(pending).resolves.toBeNull() expect(isPluginEnabled('usb')).toBe(true) }) it('retries on the next navigation after a hung request', async () => { enabled.mockReturnValueOnce(new Promise(() => {})) const first = loadEnabledPlugins() await vi.advanceTimersByTimeAsync(5000) await first // The dead promise must not be cached, or every later navigation inherits // it and stays frozen even once the backend is back. enabled.mockResolvedValueOnce({ data: { data: ['machines'] } }) await loadEnabledPlugins() expect(enabled).toHaveBeenCalledTimes(2) expect(isPluginEnabled('machines')).toBe(true) expect(isPluginEnabled('usb')).toBe(false) }) it('fetches once while the request is healthy', async () => { enabled.mockResolvedValue({ data: { data: ['machines'] } }) await loadEnabledPlugins() await loadEnabledPlugins() expect(enabled).toHaveBeenCalledTimes(1) }) })