go-learn/packages/ai/src/worker.test.ts
sab.code.lab 1c85091186 chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца
(снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной
истории.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 09:34:02 +02:00

130 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Тесты протокола воркера через фейк-порт (без реального 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);
});
});