Files
Dune/tests/deferred-movement-arrival.test.ts
Quintine 903d861170 Prevent saved CHOAM movement arrival deadlocks
Preflight ordinary movement through the existing arrival quote before creating new Ornithopter escrow or opening Ixian, Fremen and CHOAM prevention windows. An explicit legacy CHOAM decline can return an uncommitted unsupported arrival for another choice without consuming movement or touching forces, spice, cards or tokens. Preserve an existing Ornithopter and retire it exactly once on a legal replacement move. Keep dedicated Ambassador relocation handling and Karama cancellation validation before cost; unrelated exceptions remain atomic. Simultaneous Ambassador/Terror priority still awaits the recorded material interpretation, with no invented ordering.

Add reusable offline faction-game samples with genuine Basic/Advanced setup, all four AI profiles, physical custody checks, JSON restorations, private traces and failed snapshots. Source-bound reports and input snapshot hashes make fresh and resumed samples repeatable; document that a resumed continuation starts a new seeded stream and does not prove input setup provenance.

Validation: independently reviewed fresh declaration, native cancellation, supported single arrivals, legacy retry and card custody tests. Two production SQLite tests cover authenticated private restoration, stale/duplicate/wrong-owner rejection, resource conservation and concurrent version races. Typecheck, lint, 4583 offline tests, production build and 43 HTTP tests passed. Retain the initial failed check log for the corrected numeric-sort lint issue.

All six fresh samples complete. CHOAM Basic 1054 actions/28 JSON restores, CHOAM Advanced 225/6, Ecaz Basic 91/2, Ecaz Advanced 523/14, all-three Basic 476/12, all-three Advanced 1303/35 with a turn-six CHOAM win. Combined Advanced has 46 guarded arrival candidates with legal alternatives. The exact prior 614-action failed snapshot separately finishes after 691 additional actions/18 restores and 39 guarded candidates. These are bounded paths, not complete faction or combination certification. Gameplay source hashes match the final checkpoint; later evidence-only documentation and recovery-test lint edits are explicit.

After the reported outage, all 632 opening games and original seat/recovery records were preserved (665 after HTTP tests); integrity and foreign keys passed. Existing Ecaz and Ixian browser seats restored their same private hands, resources and pending choices after reload. This is browser restoration evidence, not a staged arrival-return claim. The healthy server was reused; no restart, database reset, recurring automation or publication.

Verified source tree: 550221850f4802c41155f7bbc35d623965bc7db3330825636d50f26bb42868f8
2026-09-13 23:15:00 +10:00

213 lines
7.8 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
applyAction,
normalizeAutomaticGame,
viewGame,
type Action,
type Game,
} from '../game/engine';
import {
addArrivalAmbassador,
addArrivalTerror,
arrivalArmy,
arrivalMove,
arrivalPlayer,
arrivalReload,
arrivalResources,
holdArrivalCard,
movementArrivalGame,
nativeArrivalSource,
} from './deferred-movement-arrival-fixture';
function reject(g: Game, player: string, action: Action, pattern?: RegExp) {
const before = structuredClone(g);
if (pattern) assert.throws(() => applyAction(g, player, action), pattern);
else assert.throws(() => applyAction(g, player, action));
assert.deepEqual(g, before);
}
const overlap = /Ambassadors combined with another arrival reaction/;
void test('the reproduced Tleilaxu arrival and native Ix/Fremen movement declarations reject known unsupported overlap before prevention windows or RNG', (t) => {
const games = ['tleilaxu', 'ixians', 'fremen'].map((faction) => {
const g = movementArrivalGame(faction as 'tleilaxu' | 'ixians' | 'fremen');
if (faction !== 'tleilaxu') nativeArrivalSource(g);
if (faction === 'fremen') addArrivalAmbassador(g, 'atreides');
holdArrivalCard(g, 'choam', 'Karama');
return g;
});
const random = t.mock.method(crypto, 'getRandomValues', () => {
throw Error('Unsupported arrival sampled RNG');
});
const uuid = t.mock.method(crypto, 'randomUUID', () => {
throw Error('Unsupported arrival allocated an event');
});
for (const g of games) {
reject(g, g.active!, arrivalMove(g), overlap);
assert.equal(g.pendingChoamMove, undefined);
assert.equal(g.pendingIxMove, undefined);
assert.equal(g.pendingFremenMove, undefined);
assert.equal(g.response, null);
assert.equal(g.decision, null);
}
assert.equal(random.mock.callCount(), 0);
assert.equal(uuid.mock.callCount(), 0);
});
void test('known unsupported entry rejects before putting a newly played Ornithopter into escrow or creating its event', (t) => {
const g = movementArrivalGame(),
card = holdArrivalCard(g, 'tleilaxu', 'Ornithopter');
const uuid = t.mock.method(crypto, 'randomUUID', () => {
throw Error('Movement card allocated an event');
});
reject(
g,
'tleilaxu',
{ ...arrivalMove(g), movementCard: card, ornithopter: 'range3' },
overlap,
);
assert.ok(arrivalPlayer(g, 'tleilaxu').hand.some((c) => c.id === card));
assert.equal(g.ornithopter, undefined);
assert.equal(uuid.mock.callCount(), 0);
});
for (const tokens of ['ambassador', 'terror'] as const)
for (const cancellation of [false, true])
void test(`${tokens}-only entry stays legal through CHOAM ${cancellation ? 'Baliset cancellation' : 'decline'} and commits once`, () => {
let g = movementArrivalGame('tleilaxu', tokens);
const baliset = holdArrivalCard(g, 'choam', 'Baliset'),
karama = holdArrivalCard(g, 'tleilaxu', 'Karama');
g = applyAction(g, 'tleilaxu', arrivalMove(g));
assert.equal(g.decision?.kind, 'choamMovement');
assert.equal(arrivalPlayer(g, 'tleilaxu').moved, 0);
if (cancellation) {
g = applyAction(g, 'choam', {
type: 'card',
card: baliset,
mode: 'choam',
target: 'tleilaxu',
territory: 'carthag',
});
assert.equal(g.response?.kind, 'choamWorthless');
g = applyAction(arrivalReload(g), 'tleilaxu', {
type: 'card',
card: karama,
mode: 'cancel',
});
assert.equal(g.discard.filter((c) => c.id === karama).length, 1);
assert.ok(arrivalPlayer(g, 'choam').hand.some((c) => c.id === baliset));
} else
g = applyAction(arrivalReload(g), 'choam', {
type: 'decision',
decline: true,
});
assert.equal(g.pendingChoamMove, null);
assert.equal(
g.decision?.kind,
tokens === 'ambassador' ? 'ecazAmbassador' : 'moritaniTerror',
);
assert.deepEqual(arrivalPlayer(g, 'tleilaxu').forces, {
'carthag:11': 3,
});
assert.equal(arrivalPlayer(g, 'tleilaxu').moved, 1);
const event =
tokens === 'ambassador' ? g.pendingAmbassador!.event : undefined;
g = applyAction(
arrivalReload(g),
tokens === 'ambassador' ? 'ecaz' : 'moritani',
{ type: 'decision', event, decline: true },
);
assert.equal(g.decision, null);
assert.equal(g.active, 'tleilaxu');
assert.equal(arrivalPlayer(g, 'tleilaxu').moved, 1);
reject(g, 'choam', { type: 'decision', decline: true });
});
void test('a saved CHOAM decline releases an uncommitted unsupported arrival with its precise reason and allows another legal destination', () => {
let g = movementArrivalGame('tleilaxu', 'ambassador');
const card = holdArrivalCard(g, 'tleilaxu', 'Ornithopter');
g = applyAction(g, 'tleilaxu', {
...arrivalMove(g),
movementCard: card,
ornithopter: 'range3',
});
const flight = structuredClone(g.ornithopter);
// Model the persisted checkpoint: the unsupported second token was already
// present in a legacy saved CHOAM window, whose declaration spent nothing.
addArrivalTerror(g);
const before = structuredClone(g),
resources = structuredClone(arrivalResources(g));
for (const p of g.players)
assert.deepEqual(viewGame(arrivalReload(g), p.id), viewGame(g, p.id));
assert.deepEqual(normalizeAutomaticGame(arrivalReload(g)), arrivalReload(g));
reject(g, 'tleilaxu', { type: 'decision', decline: true });
reject(g, 'choam', { type: 'decision', decline: false });
const done = applyAction(arrivalReload(g), 'choam', {
type: 'decision',
decline: true,
});
assert.deepEqual(g, before);
assert.deepEqual(arrivalResources(done), resources);
assert.equal(done.pendingChoamMove, null);
assert.equal(done.decision, null);
assert.deepEqual(done.ornithopter, flight);
assert.equal(
arrivalPlayer(done, 'tleilaxu').hand.some((c) => c.id === card),
false,
);
assert.equal(
done.discard.some((c) => c.id === card),
false,
);
assert.match(done.log.at(-1)!.text, overlap);
assert.match(
done.log.at(-1)!.text,
/No additional forces, spice, cards or movement allowance were spent/,
);
assert.deepEqual(
normalizeAutomaticGame(arrivalReload(done)),
arrivalReload(done),
);
reject(done, 'choam', { type: 'decision', decline: true });
reject(done, 'tleilaxu', arrivalMove(done), overlap);
const retry = applyAction(done, 'tleilaxu', {
type: 'move',
forces: { 'arrakeen:10': 3 },
territory: 'imperial_basin',
sector: 10,
ornithopterEvent: done.ornithopter!.event,
});
assert.equal(arrivalPlayer(retry, 'tleilaxu').moved, 1);
assert.deepEqual(arrivalPlayer(retry, 'tleilaxu').forces, {
'imperial_basin:10': 3,
});
assert.equal(retry.ornithopter, null);
assert.equal(retry.discard.filter((c) => c.id === card).length, 1);
});
void test('legacy invalid-order retry stays unspent while unexpected malformed arrival faults remain rejected', () => {
let g = movementArrivalGame('tleilaxu', 'ambassador');
g = applyAction(g, 'tleilaxu', arrivalMove(g));
const invalid = arrivalReload(g);
arrivalArmy(invalid, 'tleilaxu', 'arrakeen:10', 2);
const retried = applyAction(invalid, 'choam', {
type: 'decision',
decline: true,
});
assert.equal(retried.pendingChoamMove, null);
assert.equal(arrivalPlayer(retried, 'tleilaxu').moved, 0);
assert.deepEqual(arrivalPlayer(retried, 'tleilaxu').forces, {
'arrakeen:10': 2,
});
const malformed = arrivalReload(g);
malformed.ecazAmbassadors!.tokens = {} as NonNullable<
Game['ecazAmbassadors']
>['tokens'];
const before = structuredClone(malformed);
assert.throws(
() => applyAction(malformed, 'choam', { type: 'decision', decline: true }),
TypeError,
);
assert.deepEqual(malformed, before);
});