AUDIT / src /lib /runtime /__tests__ /workerManager.test.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 4)
641b62c verified
Raw
History Blame Contribute Delete
8.92 kB
/**
* 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);
});