/** * Тесты протокола воркера через фейк-порт (без реального Worker): * choose → result с тем же requestId; нелегальный список → error; cancel * активного → result с лучшим найденным; cancel чужого id → без эффекта. */ import type { Clock } from '@go-learn/core'; import { describe, expect, it } from 'vitest'; import type { WorkerChooseRequest, WorkerRequest, WorkerResponse } from './worker-protocol.js'; import type { MessagePort } from './worker.js'; import { createWorkerHandler } from './worker.js'; /** Фейк-порт: собирает все ответы воркера. */ function fakePort(): { port: MessagePort; messages: WorkerResponse[] } { const messages: WorkerResponse[] = []; return { port: { postMessage: (msg) => messages.push(msg) }, messages }; } /** Базовый choose-запрос с переопределениями. */ function chooseMsg(overrides: Partial = {}): WorkerChooseRequest { return { type: 'choose', requestId: 1, size: 9, komi: 5.5, moves: [], level: 4, seed: 42, timeBudgetMs: 300, ...overrides, }; } /** Часы-счётчик: +100 мс на каждый вызов now() — остановка по deadline. */ function countingClock(onCall?: (calls: number) => void): Clock { let t = 0; let calls = 0; return { now: (): number => { calls += 1; onCall?.(calls); return (t += 100); }, }; } describe('worker-протокол через фейк-порт', () => { it('choose → result с тем же requestId и легальным ходом', () => { const { port, messages } = fakePort(); const handler = createWorkerHandler(port, { clock: countingClock() }); handler( chooseMsg({ moves: [ { kind: 'play', color: 'black', point: { x: 2, y: 2 } }, { kind: 'play', color: 'white', point: { x: 6, y: 6 } }, ], }), ); expect(messages).toHaveLength(1); const response = messages[0]; expect(response?.type).toBe('result'); expect(response?.requestId).toBe(1); if (response?.type === 'result') { expect(response.simulations).toBeGreaterThanOrEqual(1); expect(response.topMoves.length).toBeLessThanOrEqual(3); } }); it('нелегальный ход в списке → error с номером хода', () => { const { port, messages } = fakePort(); const handler = createWorkerHandler(port, { clock: countingClock() }); handler( chooseMsg({ moves: [ { kind: 'play', color: 'black', point: { x: 2, y: 2 } }, { kind: 'play', color: 'white', point: { x: 2, y: 2 } }, // занято ], }), ); expect(messages).toEqual([ expect.objectContaining({ type: 'error', requestId: 1, message: expect.stringContaining('#2'), }), ]); }); it('список ходов до конца партии → error (партия завершена)', () => { const { port, messages } = fakePort(); const handler = createWorkerHandler(port, { clock: countingClock() }); handler( chooseMsg({ moves: [ { kind: 'pass', color: 'black' }, { kind: 'pass', color: 'white' }, ], }), ); expect(messages[0]?.type).toBe('error'); }); it('cancel активного requestId → result с лучшим найденным (не error)', () => { const { port, messages } = fakePort(); let handler: (msg: WorkerRequest) => void; // Реентерабельная отмена: на 3-м вызове now() (внутри счёта) шлём cancel. const clock = countingClock((calls) => { if (calls === 3) handler({ type: 'cancel', requestId: 7 }); }); handler = createWorkerHandler(port, { clock }); handler(chooseMsg({ requestId: 7, timeBudgetMs: 1_000_000 })); // «бесконечный» бюджет expect(messages).toHaveLength(1); const response = messages[0]; expect(response?.type).toBe('result'); expect(response?.requestId).toBe(7); if (response?.type === 'result') expect(response.simulations).toBeLessThanOrEqual(5); }); it('cancel чужого requestId → без эффекта: choose завершается обычным result', () => { const { port, messages } = fakePort(); const handler = createWorkerHandler(port, { clock: countingClock() }); handler({ type: 'cancel', requestId: 999 }); expect(messages).toHaveLength(0); // cancel сам по себе не порождает ответа handler(chooseMsg({ requestId: 2 })); expect(messages).toHaveLength(1); const response = messages[0]; expect(response?.type).toBe('result'); expect(response?.requestId).toBe(2); if (response?.type === 'result') expect(response.simulations).toBeGreaterThanOrEqual(1); }); });