41 lines
3.0 KiB
TypeScript
41 lines
3.0 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createResultReporter } from '../src/client/result-reporter.js';
|
|
import { emptyPlayerStats, type ResultSubmission } from '../src/shared/p2p-types.js';
|
|
import { PENDING_RESULT_KEY } from '../constants/recording.js';
|
|
|
|
function storage() {
|
|
const data = new Map<string, string>();
|
|
return { getItem: (key: string) => data.get(key) ?? null, setItem: (key: string, value: string) => { data.set(key, value); }, removeItem: (key: string) => { data.delete(key); } };
|
|
}
|
|
const result: ResultSubmission = { matchId: 'm1', seq: 1, mode: 'friend', outcome: 'win', reason: 'hit' };
|
|
test('lost result response retries identical payload and clears persisted result only after acknowledgement', async () => {
|
|
const store = storage(), sent: ResultSubmission[] = []; const timers: { fn: () => void; ms: number }[] = [];
|
|
const reporter = createResultReporter({ gameSessionId: 's1', record: async data => {
|
|
sent.push(structuredClone(data)); if (sent.length === 1) throw new Error('network');
|
|
return { status: 'duplicate', stats: emptyPlayerStats() };
|
|
} }, store, { setTimeout: (fn, ms) => { timers.push({ fn, ms }); return timers.length; }, clearTimeout: () => {} });
|
|
const promise = reporter.submit(result);
|
|
await new Promise(r => setImmediate(r));
|
|
assert.ok(store.getItem(PENDING_RESULT_KEY)); assert.equal(timers[0].ms, 2000);
|
|
timers.shift()!.fn();
|
|
assert.equal((await promise).status, 'duplicate');
|
|
assert.deepEqual(sent, [result, result]); assert.equal(store.getItem(PENDING_RESULT_KEY), null);
|
|
reporter.destroy();
|
|
});
|
|
test('reload resumes the pending result but a different session never submits it', async () => {
|
|
const store = storage(); store.setItem(PENDING_RESULT_KEY, JSON.stringify({ gameSessionId: 's1', result }));
|
|
const sent: ResultSubmission[] = [];
|
|
const wrong = createResultReporter({ gameSessionId: 's2', record: async data => { sent.push(data); return { status: 'applied', stats: emptyPlayerStats() }; } }, store);
|
|
await assert.rejects(wrong.resumePending(), /SESSION_MISMATCH/); assert.equal(sent.length, 0); wrong.destroy();
|
|
const correct = createResultReporter({ gameSessionId: 's1', record: async data => { sent.push(data); return { status: 'applied', stats: emptyPlayerStats() }; } }, store);
|
|
assert.equal((await correct.resumePending())?.status, 'applied'); assert.deepEqual(sent, [result]); correct.destroy();
|
|
});
|
|
test('permanent rejection stops retries and a different result cannot replace an in-flight one', async () => {
|
|
const store = storage(); let count = 0;
|
|
const reporter = createResultReporter({ gameSessionId: 's1', record: async () => { count++; throw Object.assign(new Error('INVALID_SESSION'), { status: 401 }); } }, store);
|
|
const first = reporter.submit(result);
|
|
await assert.rejects(reporter.submit({ ...result, matchId: 'm2' }), /RESULT_PENDING/);
|
|
await assert.rejects(first, /INVALID_SESSION/); assert.equal(count, 1); assert.ok(store.getItem(PENDING_RESULT_KEY)); reporter.destroy();
|
|
});
|