chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
1c85091186
184 changed files with 33303 additions and 0 deletions
130
packages/ai/src/worker.test.ts
Normal file
130
packages/ai/src/worker.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue