chore: восстановление репозитория из снапшота v0.3.1

Прежняя git-история утрачена при переносе проекта на машину владельца
(снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной
истории.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
sab.code.lab 2026-08-07 09:34:02 +02:00
commit 1c85091186
184 changed files with 33303 additions and 0 deletions

View file

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