Spaces:
Sleeping
Sleeping
| /** | |
| * 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<string, { pendingPing: unknown; status: { lastPingMs: number | null } }> }).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); | |
| }); | |