Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
/**
|
||
* Тесты протокола воркера через фейк-порт (без реального 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> = {}): 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);
|
||
});
|
||
});
|