/** * workerManager.test.ts — Unit test S37 WorkerManager Safari-safe * * Copre: * - ensureSpawned: spawn via factory iniettata (DI) * - ensureSpawned: ritorna stesso worker se già spawned * - ensureSpawned: resetta lastUsedAt ad ogni chiamata * - terminate: rimuove handle, chiama worker.terminate() * - terminateAll: termina tutti i worker registrati * - getStatus: "unknown" per worker non spawned * - getStatus: spawned=true dopo ensureSpawned * - onStatusChange: listener chiamato su notify (onerror) * - Idle timer: worker terminato dopo idle timeout (fake timers) * - Idle timer reset: ensureSpawned posticipa l'idle timeout * - isWorkerSupported: false in Node.js test environment * - Risposta "ready": health diventa "alive" * - Risposta "pong": lastPingMs viene settato * * Pattern DI: _setWorkerFactory(mockFactory) / _resetWorkerFactory() * Fake timers: vi.useFakeTimers() per controllare idle timeout senza attesa reale * * SESSIONE: S37 — WorkerManager Safari-safe * S246-Fix: factory ora riceve WorkerType invece di url string */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { WorkerManager, _setWorkerFactory, _resetWorkerFactory, } from "@/lib/runtime/WorkerManager"; import type { WorkerLike, WorkerType } from "@/lib/runtime/WorkerManager"; // ─── Mock Worker factory ────────────────────────────────────────────────────── interface MockWorker extends WorkerLike { _sentMessages: unknown[]; _triggerMessage: (data: unknown) => void; _triggerError: () => void; } function makeMockWorker(): MockWorker { const _sentMessages: unknown[] = []; let _onmessage: ((ev: MessageEvent) => void) | null = null; let _onerror: ((ev: ErrorEvent) => void) | null = null; const mock: MockWorker = { _sentMessages, get onmessage() { return _onmessage; }, set onmessage(fn) { _onmessage = fn as ((ev: MessageEvent) => void) | null; }, get onerror() { return _onerror; }, set onerror(fn) { _onerror = fn as ((ev: ErrorEvent) => void) | null; }, postMessage(msg: unknown) { _sentMessages.push(msg); }, terminate: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), _triggerMessage(data: unknown) { _onmessage?.({ data } as MessageEvent); }, _triggerError() { _onerror?.({} as ErrorEvent); }, }; return mock; } // S246-Fix: factory riceve WorkerType (non url string) let lastMock: MockWorker; function mockFactory(_type: WorkerType): MockWorker { lastMock = makeMockWorker(); return lastMock; } // ─── Setup / Teardown ───────────────────────────────────────────────────────── let wm: WorkerManager; beforeEach(() => { vi.useFakeTimers(); _setWorkerFactory(mockFactory); wm = new WorkerManager(500); // idle timeout breve (500ms) per test veloci }); afterEach(() => { wm.terminateAll(); _resetWorkerFactory(); vi.useRealTimers(); vi.restoreAllMocks(); }); // ─── getStatus ──────────────────────────────────────────────────────────────── it("1. getStatus: ritorna status 'unknown' per worker non spawned", () => { const s = wm.getStatus("js"); expect(s.spawned).toBe(false); expect(s.health).toBe("unknown"); expect(s.spawnCount).toBe(0); expect(s.lastUsedAt).toBeNull(); }); it("2. getStatus: spawned=true e health='unknown' subito dopo ensureSpawned", () => { wm.ensureSpawned("js"); const s = wm.getStatus("js"); expect(s.spawned).toBe(true); expect(s.health).toBe("unknown"); // nessun "ready" ricevuto ancora }); // ─── ensureSpawned ──────────────────────────────────────────────────────────── it("3. ensureSpawned: usa factory iniettata — postMessage ping iniziale", () => { wm.ensureSpawned("js"); expect(lastMock._sentMessages).toHaveLength(1); expect((lastMock._sentMessages[0] as { type: string }).type).toBe("ping"); }); it("4. ensureSpawned: ritorna lo stesso worker alla seconda chiamata", () => { const w1 = wm.ensureSpawned("js"); const w2 = wm.ensureSpawned("js"); expect(w1).toBe(w2); }); it("5. ensureSpawned: aggiorna lastUsedAt ad ogni chiamata", () => { wm.ensureSpawned("js"); const t1 = wm.getStatus("js").lastUsedAt!; vi.advanceTimersByTime(100); wm.ensureSpawned("js"); const t2 = wm.getStatus("js").lastUsedAt!; expect(t2).toBeGreaterThanOrEqual(t1); }); it("6. ensureSpawned: spawnCount incrementa solo al primo spawn", () => { wm.ensureSpawned("js"); wm.ensureSpawned("js"); expect(wm.getStatus("js").spawnCount).toBe(1); }); // ─── ready / pong ───────────────────────────────────────────────────────────── it("7. messaggio 'ready': health diventa 'alive', failedPings = 0", () => { wm.ensureSpawned("js"); lastMock._triggerMessage({ type: "ready" }); const s = wm.getStatus("js"); expect(s.health).toBe("alive"); expect(s.failedPings).toBe(0); }); it("8. messaggio 'pong': lastPingMs viene settato, health rimane alive", () => { wm.ensureSpawned("js"); lastMock._triggerMessage({ type: "ready" }); const handle = (wm as unknown as { handles: Map }).handles.get("js")!; handle.pendingPing = { resolve: vi.fn(), reject: vi.fn(), sentAt: Date.now() - 42 }; lastMock._triggerMessage({ type: "pong" }); expect(wm.getStatus("js").lastPingMs).toBeGreaterThanOrEqual(40); }); // ─── terminate ──────────────────────────────────────────────────────────────── it("9. terminate: rimuove handle e chiama worker.terminate()", () => { wm.ensureSpawned("js"); expect(wm.getStatus("js").spawned).toBe(true); wm.terminate("js"); expect(wm.getStatus("js").spawned).toBe(false); expect(lastMock.terminate).toHaveBeenCalled(); }); it("10. terminateAll: termina tutti i worker registrati", () => { const m1 = (wm.ensureSpawned("js") as MockWorker); const m2 = (wm.ensureSpawned("py") as MockWorker); wm.terminateAll(); expect(m1.terminate).toHaveBeenCalled(); expect(m2.terminate).toHaveBeenCalled(); expect(wm.getStatus("js").spawned).toBe(false); expect(wm.getStatus("py").spawned).toBe(false); }); // ─── onStatusChange ─────────────────────────────────────────────────────────── it("11. onStatusChange: listener chiamato quando onerror triggera", () => { const changes: string[] = []; wm.onStatusChange(s => changes.push(s.health)); wm.ensureSpawned("js"); lastMock._triggerError(); expect(changes).toContain("dead"); }); it("12. onStatusChange: unsub rimuove il listener", () => { const changes: string[] = []; const unsub = wm.onStatusChange(s => changes.push(s.health)); unsub(); wm.ensureSpawned("js"); lastMock._triggerMessage({ type: "ready" }); expect(changes).toHaveLength(0); }); // ─── Idle timeout (S37) ──────────────────────────────────────────────────────── it("13. idle timeout: worker terminato automaticamente dopo idleTimeoutMs", () => { wm.ensureSpawned("js"); lastMock._triggerMessage({ type: "ready" }); vi.advanceTimersByTime(499); expect(wm.getStatus("js").spawned).toBe(true); vi.advanceTimersByTime(2); expect(wm.getStatus("js").spawned).toBe(false); expect(lastMock.terminate).toHaveBeenCalled(); }); it("14. idle timer reset: ensureSpawned posticipa l'idle timeout", () => { wm.ensureSpawned("js"); lastMock._triggerMessage({ type: "ready" }); vi.advanceTimersByTime(400); wm.ensureSpawned("js"); vi.advanceTimersByTime(400); expect(wm.getStatus("js").spawned).toBe(true); vi.advanceTimersByTime(200); expect(wm.getStatus("js").spawned).toBe(false); }); // ─── isWorkerSupported ──────────────────────────────────────────────────────── it("15. isWorkerSupported: false in ambiente Node.js (senza browser Worker)", () => { expect(WorkerManager.isWorkerSupported()).toBe(false); });