import assert from 'node:assert/strict'; import test from 'node:test'; import type { DatabaseSync, SQLOutputValue } from 'node:sqlite'; import { shuffle, viewGame, type Action, type Game } from '../game/engine'; import { NEXUS_FACTIONS, validateNexusCards } from '../game/nexus-cards'; import type { RoomsClock, SeatAuth } from '../db/rooms'; import type * as Rooms from '../db/rooms'; import { startPrototypeRoom } from '../tools/prototype-room'; import { unitStore } from './fixture-nexus-room-store'; import { createHarkonnenNexusBetrayalFixture, nextHarkonnenBetrayalNativeStep, type HarkonnenBetrayalFixture, type HarkonnenBetrayalFixtureOptions as Options, } from './fixture-nexus-harkonnen-betrayal'; type SqlRow = Record; interface Fixture extends HarkonnenBetrayalFixture { sqlite: DatabaseSync; restart(): typeof Rooms; hooks: { beforeWrite?: () => Promise; afterStatement?: (sql: string) => Promise }; code: string; tokens: string[]; auths: SeatAuth[]; save(game: Game): void; protectedRows: Record; unrelatedCode: string; unrelatedToken: string; stock: string[]; traitorStock: string[]; setup: Game; initial: Game; } const clock: RoomsClock = { now: () => 10000, sleep: async () => {} }; const plain = (value: T): T => JSON.parse(JSON.stringify(value)); /** Compare durable records, including credentials, lifecycle and recovery resources, * rather than treating an attempted SQL update as a committed game action. */ function records(sqlite: DatabaseSync, excludedRoom?: string) { const result: Record = {}; for (const row of sqlite.prepare("SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all()) { assert.equal(typeof row.name, 'string'); const table = String(row.name).replaceAll('"', '""'); const rows = sqlite.prepare(`SELECT * FROM "${table}"`).all(); result[String(row.name)] = rows.filter(value => !(row.name === 'rooms' && value.code === excludedRoom)); } return result; } function physical(game: Game) { const cards = [...game.deck, ...game.discard, ...game.players.flatMap(player => player.hand), ...(game.auction?.cards.slice(game.auction.index + Number(game.currentAuctionSale?.origin === 'normal')) ?? [])]; const ids = cards.map(card => card.id).sort(); assert.equal(new Set(ids).size, ids.length, 'native Treachery cards have one physical custodian'); return ids; } function traitors(game: Game) { const ids = [...(game.traitorReserve ?? []), ...game.players.flatMap(player => [...player.traitors, ...player.traitorChoices])].sort(); assert.equal(new Set(ids).size, ids.length, 'each original physical Traitor Card has exactly one custodian'); return ids; } function player(game: Game, id: string) { const value = game.players.find(player => player.id === id); assert.ok(value); return value; } function inventory(f: Fixture, game: Game) { assert.deepEqual(physical(game), f.stock); assert.equal(f.stock.length, 33); assert.deepEqual(traitors(game), f.traitorStock); assert.ok(game.nexusCards?.cards); validateNexusCards(game.nexusCards.cards, game.players); assert.deepEqual([...game.nexusCards.cards.deck, ...game.nexusCards.cards.discard, ...Object.values(game.nexusCards.cards.hands).filter(face => face !== null)].sort(), [...NEXUS_FACTIONS].sort()); assert.deepEqual(records(f.sqlite, f.code), f.protectedRows); } async function authenticate(f: Fixture, id: string) { const index = f.auths.findIndex(auth => auth.playerId === id); assert.ok(index >= 0); const auth = await f.restart().authenticate(f.code, f.tokens[index]); assert.equal(auth.playerId, id); assert.equal(auth.tokenHash, f.auths[index].tokenHash); return auth; } async function act(f: Fixture, game: Game, id: string, action: Action) { await f.restart().act(f.code, await authenticate(f, id), game.version, action, clock); // The room API treats this native wake-up as read-only; its persisted worker // performs any genuine pending discard continuation through the normal CAS. if (action.type === 'advanceBots') await f.restart().continueRoomAutomatic(f.code, clock); return f.restart().readRoom(f.code); } async function nativeCounters(f: Fixture, initial: Game) { let game = initial; for (let step = 0; game.response && step < 20; step++) { const responder = game.players.find(player => !game.response!.passed.includes(player.id)); assert.ok(responder); game = await act(f, game, responder.id, { type: 'passResponse' }); } assert.equal(game.response, null); return game; } async function openGate(f: Fixture, beforeCall: Game) { let game = await act(f, beforeCall, f.provider, f.callAction); if (f.remote) { if (game.response) { assert.equal(game.response.kind, 'harkonnenTraitor'); assert.equal(viewGame(game, f.holder).nexusHarkonnenBetrayalReaction, null); } else { assert.equal(game.pendingNexusHarkonnenBetrayal!.source.nativeWindow, 'harkonnenTraitor'); assert.equal(game.pendingNexusHarkonnenBetrayal!.source.nativeRequired.length, 0, 'Only a real counter-free native call may reach Nexus automatically.'); } } game = await nativeCounters(f, game); assert.ok(game.pendingNexusHarkonnenBetrayal); assert.equal(viewGame(game, f.holder).nexusHarkonnenBetrayalReaction!.event, f.event); return game; } async function fixture(t: test.TestContext, options: Options & { counterKind?: 'printed' | 'BG Worthless' } = {}): Promise { const store = unitStore(); t.after(() => store.sqlite.close()); const made = await store.rooms.createRoom('Native Harkonnen recovery', 'harkonnen', options.advanced ?? true, []); const code = made.view.code; const tokens = [made.token]; // BG's original prediction keeps the CLI-produced setup genuinely undealt; // the fixture then consumes that native setup with its scoped deterministic RNG. const factions = options.counterKind === 'printed' ? ['atreides', 'emperor'] as const : ['atreides', 'emperor', 'beneGesserit'] as const; for (const faction of factions) { const joined = await store.rooms.joinRoom(code, faction, faction); assert.ok(joined.token); tokens.push(joined.token); } const auths = await Promise.all(tokens.map(token => store.rooms.authenticate(code, token))); let admitted = await store.rooms.readRoom(code); for (const auth of auths) { await store.rooms.act(code, auth, admitted.version, { type: 'ready' }, clock); admitted = await store.rooms.readRoom(code); } // Main recovery scenarios continue the exact CLI-produced undealt setup. // Counter scenarios retain the authenticated fresh lobby so the native fixture // can conserve and order its first actual deal; no started custody is redealt. if (!options.counterKind) startPrototypeRoom(store.sqlite, code, admitted.version, 'harkonnen-betrayal'); const setup = await store.rooms.readRoom(code); const native = createHarkonnenNexusBetrayalFixture({ advanced: options.advanced, remote: options.remote, receiverCount: options.receiverCount, face: options.face, initial: setup }); assert.equal(native.beforeCall.code, code); assert.equal(native.beforeCall.host, admitted.host); assert.equal(native.beforeCall.advanced, admitted.advanced); assert.deepEqual(native.beforeCall.expansions, []); assert.deepEqual(native.beforeCall.playerPositions, admitted.playerPositions); assert.deepEqual(native.beforeCall.players.map(player => [player.id, player.name, player.faction]), admitted.players.map(player => [player.id, player.name, player.faction])); const save = (game: Game) => { assert.equal(store.sqlite.prepare('UPDATE rooms SET state=?,version=? WHERE code=?').run(JSON.stringify(game), game.version, code).changes, 1); }; save(native.beforeCall); const unrelated = await store.rooms.createRoom('Unrelated preserved lobby', 'guild', false, []); const f: Fixture = { ...store, ...native, code, tokens, auths, save, protectedRows: records(store.sqlite, code), unrelatedCode: unrelated.view.code, unrelatedToken: unrelated.token, stock: physical(native.beforeCall), traitorStock: traitors(native.beforeCall), initial: native.beforeCall, setup }; f.initial = await openGate(f, native.beforeCall); return f; } async function rejected(f: Fixture, game: Game, auth: SeatAuth, action: Action, version = game.version) { const before = records(f.sqlite); await assert.rejects(f.restart().act(f.code, auth, version, action, clock)); assert.deepEqual(records(f.sqlite), before, 'rejection changes no durable game, credential or resource record'); } async function restored(f: Fixture, game: Game) { const before = records(f.sqlite); assert.deepEqual(plain(await f.restart().readRoom(f.code)), plain(game)); for (const auth of f.auths) { const view = await f.restart().readSeatView(f.code, await authenticate(f, auth.playerId)); assert.deepEqual(plain(view), plain(viewGame(game, auth.playerId))); for (const key of ['pendingNexusHarkonnenBetrayal', 'nexusHarkonnenBetrayalCursor', 'nexusHarkonnenBetrayalHistory', 'pendingNexusHarkonnenReplacement', 'nexusHarkonnenReplacementHistory', 'traitorReserve']) assert.equal(Object.hasOwn(view, key), false, `${key} stays private`); for (const rival of view.players.filter(player => player.id !== auth.playerId)) for (const key of ['hand', 'traitors', 'spice']) assert.equal(Object.hasOwn(rival, key), false); const reaction = view.nexusHarkonnenBetrayalReaction; if (reaction) { assert.deepEqual(Object.keys(reaction).sort(), ['event', 'provider', 'beneficiary', 'target', 'leader', 'identity', 'canPass', 'hasPassed', 'canUse', 'blocked'].sort()); assert.equal(reaction.provider, f.provider); assert.equal(reaction.beneficiary, f.beneficiary); assert.equal(reaction.target, f.target); assert.equal(reaction.identity, f.identity, 'only the genuine public declaration is revealed'); if (!f.required.includes(auth.playerId)) { assert.equal(reaction.canPass, false); assert.equal(reaction.canUse, false); assert.equal(reaction.blocked, null); } } } await Promise.all([f.restart().continueRoomAutomatic(f.code, clock), f.restart().continueRoomAutomatic(f.code, clock)]); assert.deepEqual(records(f.sqlite), before, 'refresh/recovery never consumes a pending response or redraws a finished replacement'); inventory(f, game); } async function closeGate(f: Fixture, initial: Game, used: boolean) { if (used) return act(f, initial, f.holder, { type: 'nexusHarkonnenBetrayalUse', event: f.event }); let game = initial; for (const id of f.required) game = await act(f, game, id, { type: 'nexusHarkonnenBetrayalPass', event: f.event }); return game; } function settled(f: Fixture, game: Game, used: boolean) { assert.equal(game.pendingNexusHarkonnenBetrayal ?? null, null); assert.equal(viewGame(game, f.holder).nexusHarkonnenBetrayalReaction, null); const history = game.nexusHarkonnenBetrayalHistory!; assert.equal(history.length, (f.initial.nexusHarkonnenBetrayalHistory?.length ?? 0) + 1); const receipt = history.at(-1)!; assert.deepEqual(receipt.source, f.initial.pendingNexusHarkonnenBetrayal!.source); assert.equal(receipt.outcome, used ? 'use' : 'pass'); assert.equal(receipt.holder, used ? f.holder : null); assert.equal(game.nexusHarkonnenBetrayalCursor!.sequence, f.initial.nexusHarkonnenBetrayalCursor!.sequence + 1); assert.equal(game.nexusCards!.cards!.hands[f.holder], used ? null : f.initial.nexusCards!.cards!.hands[f.holder]); assert.deepEqual(game.nexusCards!.cards!.discard, [...f.initial.nexusCards!.cards!.discard, ...(used ? ['harkonnen'] : [])]); assert.deepEqual(player(game, f.provider).traitors, used ? player(f.initial, f.provider).traitors.filter(identity => identity !== f.identity) : player(f.initial, f.provider).traitors); if (used) { assert.deepEqual([...(game.traitorReserve ?? [])].sort(), [...(f.initial.traitorReserve ?? []), f.identity].sort()); assert.equal(player(game, f.provider).revealedTraitors?.includes(f.identity) ?? false, false); assert.equal(game.pendingNexusHarkonnenReplacement!.status, 'due'); assert.equal(game.pendingNexusHarkonnenReplacement!.identity, f.identity); assert.equal(game.pendingNexusHarkonnenReplacement!.provider, f.provider); assert.equal(game.pendingNexusHarkonnenReplacement!.turn, f.initial.turn); assert.equal(game.pendingNexusHarkonnenReplacement!.drawn, null); assert.equal(game.battle?.traitorCalls[f.provider], false, 'the original call is canceled, not replayed'); } else { assert.equal(game.pendingNexusHarkonnenReplacement ?? null, null); assert.deepEqual(game.traitorReserve, f.initial.traitorReserve); } inventory(f, game); } async function mentat(f: Fixture, initial: Game, raceRecovery = false) { let game = initial; for (let step = 0; game.phase !== 8 && step < 80; step++) { const next = nextHarkonnenBetrayalNativeStep(game); const before = game; if (raceRecovery) { await Promise.all([act(f, before, next.actor, next.action), f.restart().continueRoomAutomatic(f.code, clock), f.restart().continueRoomAutomatic(f.code, clock)]); game = await f.restart().readRoom(f.code); } else game = await act(f, before, next.actor, next.action); } assert.equal(game.phase, 8, 'actual battle resolution, Collection and native readiness reach Mentat'); assert.equal(game.turn, initial.turn); assert.equal(game.battle, null); return game; } for (const advanced of [false, true]) for (const remote of [false, true]) for (const used of [false, true]) { void test(`SQLite ${advanced ? 'Advanced' : 'Basic'} ${remote ? 'allied' : 'personal'} Harkonnen ${used ? 'Use' : 'all-pass'} continues the original battle and actual Mentat once`, async t => { const f = await fixture(t, { advanced, remote }); await restored(f, f.initial); const done = await closeGate(f, f.initial, used); settled(f, done, used); await restored(f, done); const expectedDraw = done.traitorReserve?.[0]; const previousHeld = [...player(done, f.provider).traitors]; const pause = await mentat(f, done, true); assert.equal(pause.pendingNexusHarkonnenReplacement ?? null, null); if (used) { const drawn = pause.nexusHarkonnenReplacementHistory!.at(-1)!; assert.equal(drawn.status, 'drawn'); assert.equal(drawn.event, f.event); assert.equal(drawn.drawn, expectedDraw); assert.deepEqual(player(pause, f.provider).traitors, [...previousHeld, expectedDraw]); assert.deepEqual(pause.traitorReserve, done.traitorReserve!.slice(1)); assert.equal(pause.nexusHarkonnenReplacementHistory!.length, (done.nexusHarkonnenReplacementHistory?.length ?? 0) + 1); } else { assert.deepEqual(pause.nexusHarkonnenReplacementHistory, done.nexusHarkonnenReplacementHistory); assert.deepEqual(player(pause, f.provider).traitors, previousHeld); } await restored(f, pause); for (const type of ['nexusHarkonnenBetrayalUse', 'nexusHarkonnenBetrayalPass'] as const) await rejected(f, pause, await authenticate(f, f.holder), { type, event: f.event }); }); } void test('SQLite lost successful Use response restores the exact committed shuffle and cannot spend or schedule twice', async t => { const f = await fixture(t, { remote: true }); let lost = false; f.hooks.afterStatement = async sql => { if (sql.startsWith('UPDATE rooms SET state') && !lost) { lost = true; throw new Error('lost response after committed CAS'); } }; try { await assert.rejects(f.restart().act(f.code, await authenticate(f, f.holder), f.initial.version, { type: 'nexusHarkonnenBetrayalUse', event: f.event }, clock), /lost response/); } finally { delete f.hooks.afterStatement; } const committed = await f.restart().readRoom(f.code); assert.equal(committed.version, f.initial.version + 1); settled(f, committed, true); await restored(f, committed); await rejected(f, committed, await authenticate(f, f.holder), { type: 'nexusHarkonnenBetrayalUse', event: f.event }, f.initial.version); await rejected(f, committed, await authenticate(f, f.holder), { type: 'nexusHarkonnenBetrayalUse', event: f.event }); }); void test('SQLite actor credentials, revoked sessions and event-only selectors fence private state without durable changes', async t => { const f = await fixture(t); const holder = await authenticate(f, f.holder); const foreign = await f.restart().authenticate(f.unrelatedCode, f.unrelatedToken); const use: Action = { type: 'nexusHarkonnenBetrayalUse', event: f.event }; const before = records(f.sqlite); await assert.rejects(f.restart().authenticate(f.code, f.unrelatedToken)); await assert.rejects(f.restart().readSeatView(f.code, foreign)); await assert.rejects(f.restart().readSeatView(f.code, { playerId: f.holder, tokenHash: foreign.tokenHash })); assert.deepEqual(records(f.sqlite), before); for (const auth of [foreign, { playerId: f.holder, tokenHash: foreign.tokenHash }, await authenticate(f, f.provider)]) await rejected(f, f.initial, auth, use); await rejected(f, f.initial, holder, use, f.initial.version - 1); for (const extra of [{ actor: f.provider }, { player: f.provider }, { provider: f.provider }, { holder: f.holder }, { identity: f.identity }, { leader: f.identity }, { target: f.target }, { card: 'harkonnen' }, { event: `${f.event}:stale` }, { replacement: f.identity }]) for (const type of ['nexusHarkonnenBetrayalUse', 'nexusHarkonnenBetrayalPass'] as const) await rejected(f, f.initial, holder, { ...use, type, ...extra } as Action); for (const action of [f.callAction, { type: 'passResponse' }, { type: 'ready' }, { type: 'traitorCall', call: false }, { type: 'nexusCard', choice: 'replace' }]) await rejected(f, f.initial, await authenticate(f, f.provider), action as Action); f.sqlite.prepare('UPDATE seats SET revoked=1 WHERE room_code=? AND player_id=?').run(f.code, f.holder); f.protectedRows = records(f.sqlite, f.code); await assert.rejects(f.restart().readSeatView(f.code, holder)); await rejected(f, f.initial, holder, use); }); void test('SQLite neutral rival acknowledgments are identical for genuine relevant and irrelevant natural Nexus draws', async t => { const f = await fixture(t); const irrelevantFixture = createHarkonnenNexusBetrayalFixture({ initial: f.setup, face: 'richese' }); const other: Fixture = { ...f, ...irrelevantFixture }; f.save(irrelevantFixture.beforeCall); const irrelevant = await openGate(other, irrelevantFixture.beforeCall); other.initial = irrelevant; for (const auth of f.auths) { const original = viewGame(f.initial, auth.playerId); const changed = await f.restart().readSeatView(f.code, await authenticate(f, auth.playerId)); if (auth.playerId !== f.holder) { const originalReaction = original.nexusHarkonnenBetrayalReaction!; const changedReaction = changed.nexusHarkonnenBetrayalReaction!; assert.deepEqual({ ...changedReaction, event: null }, { ...originalReaction, event: null }); assert.deepEqual(changed.players.map(player => [player.id, player.revealedTraitors]), original.players.map(player => [player.id, player.revealedTraitors])); assert.deepEqual(changed.nexusCards, original.nexusCards); } else { assert.equal(changed.nexusHarkonnenBetrayalReaction!.canPass, true); assert.equal(changed.nexusHarkonnenBetrayalReaction!.canUse, false); } } await restored(other, irrelevant); await rejected(other, irrelevant, await authenticate(other, other.holder), { type: 'nexusHarkonnenBetrayalUse', event: other.event }); const done = await closeGate(other, irrelevant, false); assert.equal(done.nexusCards!.cards!.hands[f.holder], 'richese'); assert.deepEqual(done.nexusCards!.cards!.discard, irrelevant.nexusCards!.cards!.discard); inventory(f, done); }); void test('SQLite two public receivers restore partial Pass, retain private faces and reject duplicate/stale acknowledgement', async t => { const f = await fixture(t, { receiverCount: 2 }); assert.equal(f.required.length, 2); const firstId = f.required.find(id => id !== f.holder)!; const first = await act(f, f.initial, firstId, { type: 'nexusHarkonnenBetrayalPass', event: f.event }); assert.deepEqual(first.pendingNexusHarkonnenBetrayal!.passed, [firstId]); assert.equal(viewGame(first, firstId).nexusHarkonnenBetrayalReaction!.hasPassed, true); assert.equal(viewGame(first, f.holder).active, f.holder); assert.deepEqual(first.nexusCards!.cards, f.initial.nexusCards!.cards); assert.deepEqual(first.nexusHarkonnenBetrayalHistory, f.initial.nexusHarkonnenBetrayalHistory); await restored(f, first); await rejected(f, first, await authenticate(f, firstId), { type: 'nexusHarkonnenBetrayalPass', event: f.event }); await rejected(f, first, await authenticate(f, f.holder), { type: 'nexusHarkonnenBetrayalUse', event: f.event }, f.initial.version); const done = await closeGate(f, first, true); settled(f, done, true); }); for (const race of ['use/pass', 'use/use', 'two passes'] as const) { void test(`SQLite concurrent ${race} commits one source transition and restores the losing actor's fresh continuation`, async t => { const f = await fixture(t, { receiverCount: race === 'two passes' ? 2 : 1 }); let arrivals = 0; let release!: () => void; const gate = new Promise(resolve => { release = resolve; }); f.hooks.beforeWrite = async () => { if (++arrivals === 2) release(); await gate; }; const ids = race === 'two passes' ? f.required : [f.holder, f.holder]; const auths = await Promise.all(ids.map(id => authenticate(f, id))); const types = race === 'two passes' ? ['nexusHarkonnenBetrayalPass', 'nexusHarkonnenBetrayalPass'] as const : race === 'use/use' ? ['nexusHarkonnenBetrayalUse', 'nexusHarkonnenBetrayalUse'] as const : ['nexusHarkonnenBetrayalUse', 'nexusHarkonnenBetrayalPass'] as const; let results: PromiseSettledResult[]; try { results = await Promise.allSettled(auths.map((auth, index) => f.restart().act(f.code, auth, f.initial.version, { type: types[index], event: f.event }, clock).finally(release))); } finally { delete f.hooks.beforeWrite; } assert.equal(arrivals, 2); assert.equal(results.filter(result => result.status === 'fulfilled').length, 1); assert.equal(results.filter(result => result.status === 'rejected').length, 1); let done = await f.restart().readRoom(f.code); assert.equal(done.version, f.initial.version + 1); if (race === 'two passes') { const winner = results.findIndex(result => result.status === 'fulfilled'); assert.deepEqual(done.pendingNexusHarkonnenBetrayal!.passed, [ids[winner]]); await restored(f, done); await rejected(f, done, auths[winner], { type: 'nexusHarkonnenBetrayalPass', event: f.event }); done = await act(f, done, ids[1 - winner], { type: 'nexusHarkonnenBetrayalPass', event: f.event }); settled(f, done, false); } else settled(f, done, race === 'use/use' || results[0].status === 'fulfilled'); await restored(f, done); }); } async function corruptRejected(f: Fixture, corrupt: Game, name: string) { f.save(corrupt); const before = records(f.sqlite); const original = plain(corrupt); for (const auth of f.auths) await assert.rejects(f.restart().readSeatView(f.code, await authenticate(f, auth.playerId)), name); for (const type of ['nexusHarkonnenBetrayalPass', 'nexusHarkonnenBetrayalUse'] as const) await rejected(f, corrupt, await authenticate(f, f.holder), { type, event: f.event }); await Promise.allSettled([f.restart().continueRoomAutomatic(f.code, clock), f.restart().continueRoomAutomatic(f.code, clock)]); assert.deepEqual(records(f.sqlite), before, `${name}: read/action/normalize never repairs or consumes corrupt saves`); assert.deepEqual(corrupt, original); } for (const remote of [false, true]) { void test(`SQLite ${remote ? 'allied' : 'personal'} committed source and physical custody corruption is immutable across authenticated read/action/recovery`, async t => { const f = await fixture(t, remote ? { remote: true, counterKind: 'printed' } : { remote: false }); if (remote) assert.ok(f.initial.pendingNexusHarkonnenBetrayal!.source.nativePassed.length, 'The lost-pass mutation requires an actual completed native counter window.'); const alterations: { name: string; change: (game: Game) => void }[] = [ { name: 'missing gate', change: game => { delete game.pendingNexusHarkonnenBetrayal; } }, { name: 'missing cursor', change: game => { delete game.nexusHarkonnenBetrayalCursor; } }, { name: 'missing history', change: game => { delete game.nexusHarkonnenBetrayalHistory; } }, { name: 'cursor sequence', change: game => { game.nexusHarkonnenBetrayalCursor!.sequence++; } }, { name: 'source parent', change: game => { game.pendingNexusHarkonnenBetrayal!.source.parent += ':forged'; } }, { name: 'native counter context', change: game => { game.pendingNexusHarkonnenBetrayal!.source.nativeContext += ':forged'; } }, { name: 'native window', change: game => { game.pendingNexusHarkonnenBetrayal!.source.nativeWindow = remote ? 'direct' : 'harkonnenTraitor'; } }, { name: 'source provider', change: game => { game.pendingNexusHarkonnenBetrayal!.source.provider = f.holder; } }, { name: 'matched identity', change: game => { game.pendingNexusHarkonnenBetrayal!.source.declaration.identity = game.traitorReserve![0]; } }, { name: 'battle event', change: game => { game.battle!.event += ':forged'; } }, { name: 'beneficiary role', change: game => { game.pendingNexusHarkonnenBetrayal!.source.declaration.beneficiary = f.holder; } }, { name: 'required membership', change: game => { game.pendingNexusHarkonnenBetrayal!.required = [f.provider]; } }, { name: 'invented pass', change: game => { game.pendingNexusHarkonnenBetrayal!.passed = [f.provider]; } }, { name: 'public declaration', change: game => { game.battle!.traitorDeclarations![f.provider].signature += ':forged'; } }, { name: 'missing called physical traitor', change: game => { player(game, f.provider).traitors = player(game, f.provider).traitors.filter(identity => identity !== f.identity); game.traitorReserve!.push(f.identity); } }, { name: 'duplicate Traitor identity', change: game => { game.traitorReserve!.push(f.identity); } }, { name: 'missing Traitor identity', change: game => { game.traitorReserve!.pop(); } }, { name: 'duplicate Nexus', change: game => { game.nexusCards!.cards!.deck.push('harkonnen'); } }, { name: 'mutated committed Nexus face', change: game => { const cards = game.nexusCards!.cards!; const index = cards.deck.indexOf('richese'); assert.ok(index >= 0); cards.hands[f.holder] = 'richese'; cards.deck[index] = 'harkonnen'; } }, { name: 'missing base Treachery', change: game => { game.deck.pop(); } }, { name: 'canonical Treachery face', change: game => { game.deck[0].name += ' forged'; } }, { name: 'lost preview boundary', change: game => { delete game.nexusHarkonnenBetrayalPreview; } }, { name: 'native faction changed', change: game => { player(game, f.provider).faction = 'guild'; } }, { name: 'foreign expansion', change: game => { game.expansions.push('ix'); } }, { name: 'wrong phase', change: game => { game.phase++; } }, { name: 'wrong turn', change: game => { game.turn++; } }, ...(remote ? [{ name: 'lost original counter passes', change: (game: Game) => { game.pendingNexusHarkonnenBetrayal!.source.nativePassed = []; } }] : []), ]; for (const { name, change } of alterations) { const corrupt = structuredClone(f.initial); change(corrupt); await corruptRejected(f, corrupt, name); } }); } void test('SQLite exact successful Use obligation restores before Mentat, privately draws once and rejects corrupted late provenance', async t => { const f = await fixture(t, { remote: true }); const due = await closeGate(f, f.initial, true); await restored(f, due); const pending = due.pendingNexusHarkonnenReplacement!; const dueAlterations: { name: string; change: (game: Game) => void }[] = [ { name: 'missing obligation', change: game => { game.pendingNexusHarkonnenReplacement = null; } }, { name: 'replacement wrong provider', change: game => { game.pendingNexusHarkonnenReplacement!.provider = f.holder; } }, { name: 'replacement wrong turn', change: game => { game.pendingNexusHarkonnenReplacement!.turn++; } }, { name: 'replacement wrong identity', change: game => { game.pendingNexusHarkonnenReplacement!.identity = game.traitorReserve!.find(identity => identity !== f.identity)!; } }, { name: 'replacement receipt link', change: game => { game.pendingNexusHarkonnenReplacement!.receipt += ':forged'; } }, { name: 'premature replacement', change: game => { game.pendingNexusHarkonnenReplacement!.status = 'drawn'; game.pendingNexusHarkonnenReplacement!.drawn = game.traitorReserve![0]; } }, { name: 'closed history link', change: game => { game.nexusHarkonnenBetrayalHistory!.at(-1)!.previous += ':forged'; } }, { name: 'closed declaration source', change: game => { game.nexusHarkonnenBetrayalHistory!.at(-1)!.source.nativeContext += ':forged'; } }, ]; for (const { name, change } of dueAlterations) { const corrupt = structuredClone(due); change(corrupt); await corruptRejected(f, corrupt, name); } f.save(due); let game = due; for (let count = 0; game.phase !== 7 && count < 80; count++) { const step = nextHarkonnenBetrayalNativeStep(game); game = await act(f, game, step.actor, step.action); } assert.equal(game.phase, 7, 'the original battle reaches actual Collection without changing the due obligation'); await restored(f, game); assert.deepEqual(game.pendingNexusHarkonnenReplacement, pending); const pause = await mentat(f, game, true); assert.equal(pause.pendingNexusHarkonnenReplacement, null); const drawn = pause.nexusHarkonnenReplacementHistory!.at(-1)!; assert.equal(drawn.receipt, pending.receipt); assert.equal(drawn.drawn, game.traitorReserve![0]); const providerView = await f.restart().readSeatView(f.code, await authenticate(f, f.provider)); assert.deepEqual(providerView.players.find(player => player.id === f.provider)!.traitors, player(pause, f.provider).traitors); for (const auth of f.auths.filter(auth => auth.playerId !== f.provider)) { const view = await f.restart().readSeatView(f.code, await authenticate(f, auth.playerId)); assert.equal(Object.hasOwn(view.players.find(player => player.id === f.provider)!, 'traitors'), false); assert.equal(Object.hasOwn(view, 'nexusHarkonnenReplacementHistory'), false); } await restored(f, pause); for (const change of [ (game: Game) => { game.nexusHarkonnenReplacementHistory = []; }, (game: Game) => { game.nexusHarkonnenReplacementHistory!.push(structuredClone(drawn)); }, (game: Game) => { game.nexusHarkonnenReplacementHistory!.at(-1)!.drawn = 'unknown-physical-identity'; }, (game: Game) => { game.nexusHarkonnenReplacementHistory!.at(-1)!.receipt += ':forged'; }, (game: Game) => { game.pendingNexusHarkonnenReplacement = structuredClone(pending); }, (game: Game) => { game.nexusHarkonnenBetrayalHistory = []; }, (game: Game) => { game.nexusHarkonnenBetrayalCursor!.signature += ':forged'; }, ]) { const corrupt = structuredClone(pause); change(corrupt); await corruptRejected(f, corrupt, 'completed replacement provenance'); } }); void test('SQLite Use shuffles the exact retired physical identity and permits its genuine same-card Mentat redraw once', async t => { const f = await fixture(t); const sourceReserve = [...f.initial.traitorReserve!, f.identity]; let quoteIndex = 0; const expected = shuffle(sourceReserve, () => quoteIndex++ === 0 ? 0 : 0xffffffff / 4294967296); assert.equal(expected[0], f.identity, 'deterministic real shuffle places the retired identity at the actual deck top'); const descriptor = Object.getOwnPropertyDescriptor(globalThis.crypto, 'getRandomValues'); let randomIndex = 0; Object.defineProperty(globalThis.crypto, 'getRandomValues', { configurable: true, value: (array: T): T => { assert.ok(array instanceof Uint32Array); for (let index = 0; index < array.length; index++) array[index] = randomIndex++ === 0 ? 0 : 0xffffffff; return array; }, }); let due: Game; try { due = await closeGate(f, f.initial, true); } finally { if (descriptor) Object.defineProperty(globalThis.crypto, 'getRandomValues', descriptor); else Reflect.deleteProperty(globalThis.crypto, 'getRandomValues'); } assert.deepEqual(due.traitorReserve, expected, 'Use performs the native shuffle on reserve plus exactly the called physical card'); assert.deepEqual(player(due, f.provider).traitors, player(f.initial, f.provider).traitors.filter(identity => identity !== f.identity)); await restored(f, due); const pause = await mentat(f, due, true); assert.equal(pause.nexusHarkonnenReplacementHistory!.at(-1)!.drawn, f.identity); assert.equal(player(pause, f.provider).traitors.filter(identity => identity === f.identity).length, 1); assert.deepEqual(pause.traitorReserve, expected.slice(1)); assert.equal(player(pause, f.provider).revealedTraitors?.includes(f.identity) ?? false, false, 'a private replacement does not turn the historically declared card into current public ownership'); await restored(f, pause); }); void test('SQLite lost final Collection response restores the original actual Mentat draw without replaying battle or phase effects', async t => { const f = await fixture(t, { remote: true }); const due = await closeGate(f, f.initial, true); let lost = false; f.hooks.afterStatement = async sql => { if (!sql.startsWith('UPDATE rooms SET state')) return; const saved = await f.restart().readRoom(f.code); if (saved.phase === 8 && !lost) { lost = true; throw new Error('lost final Collection response'); } }; try { await assert.rejects(mentat(f, due), /lost final Collection response/); } finally { delete f.hooks.afterStatement; } assert.equal(lost, true); const pause = await f.restart().readRoom(f.code); assert.equal(pause.phase, 8); assert.equal(pause.turn, due.turn); assert.equal(pause.battle, null); assert.equal(pause.pendingNexusHarkonnenReplacement, null); assert.equal(pause.nexusHarkonnenReplacementHistory!.at(-1)!.drawn, due.traitorReserve![0]); assert.deepEqual(player(pause, f.provider).traitors, [...player(due, f.provider).traitors, due.traitorReserve![0]]); assert.deepEqual(pause.traitorReserve, due.traitorReserve!.slice(1)); await restored(f, pause); }); for (const counterKind of ['printed', 'BG Worthless'] as const) { void test(`SQLite actual allied Harkonnen ${counterKind} counter preempts the Nexus acknowledgement and preserves its face`, async t => { const f = await fixture(t, { remote: true, advanced: true, counterKind }); f.save(f.beforeCall); const declared = await act(f, f.beforeCall, f.provider, f.callAction); assert.ok(declared.response); assert.equal(viewGame(declared, f.holder).nexusHarkonnenBetrayalReaction, null); const counter = counterKind === 'printed' ? declared.players.find(player => player.id !== f.provider && player.hand.some(card => card.effect === 'karama')) : declared.players.find(player => player.faction === 'beneGesserit'); assert.ok(counter); const card = counter.hand.find(card => counterKind === 'printed' ? card.effect === 'karama' : card.kind === 'worthless'); assert.ok(card, 'the conserved actual native deal supplies the counter card'); const canceled = await act(f, declared, counter.id, { type: 'card', mode: 'cancel', card: card.id }); const done = await nativeCounters(f, canceled); assert.equal(done.pendingNexusHarkonnenBetrayal ?? null, null); assert.equal(done.pendingNexusHarkonnenReplacement ?? null, null); assert.equal(viewGame(done, f.holder).nexusHarkonnenBetrayalReaction, null); assert.deepEqual(done.nexusHarkonnenBetrayalHistory, f.beforeCall.nexusHarkonnenBetrayalHistory); assert.deepEqual(done.nexusHarkonnenBetrayalCursor, f.beforeCall.nexusHarkonnenBetrayalCursor); assert.deepEqual(done.nexusCards!.cards, f.beforeCall.nexusCards!.cards); assert.deepEqual(player(done, f.provider).traitors, player(f.beforeCall, f.provider).traitors); assert.equal(done.discard.filter(value => value.id === card.id).length, 1); await restored(f, done); }); } void test('SQLite credential revocation at the final Use fence cannot commit a source, shuffle or replacement', async t => { const f = await fixture(t); const auth = await authenticate(f, f.holder); const before = f.sqlite.prepare('SELECT * FROM rooms WHERE code=?').get(f.code); let revoked = false; f.hooks.beforeWrite = async () => { assert.equal(revoked, false); revoked = true; f.sqlite.prepare('UPDATE seats SET revoked=1 WHERE room_code=? AND player_id=?').run(f.code, f.holder); f.protectedRows = records(f.sqlite, f.code); }; try { await assert.rejects(f.restart().act(f.code, auth, f.initial.version, { type: 'nexusHarkonnenBetrayalUse', event: f.event }, clock)); } finally { delete f.hooks.beforeWrite; } assert.equal(revoked, true); assert.deepEqual(f.sqlite.prepare('SELECT * FROM rooms WHERE code=?').get(f.code), before); inventory(f, await f.restart().readRoom(f.code)); await assert.rejects(f.restart().readSeatView(f.code, auth)); }); void test('SQLite every credential keeps independent own autopilot control while the original Harkonnen source remains pending', async t => { const f = await fixture(t, { receiverCount: 2 }); let game = f.initial; for (const auth of f.auths) { const previous = game; game = await act(f, game, auth.playerId, { type: 'setAutopilot', difficulty: 'Easy' }); assert.equal(player(game, auth.playerId).autopilot, 'Easy'); assert.deepEqual(game.pendingNexusHarkonnenBetrayal, previous.pendingNexusHarkonnenBetrayal); assert.deepEqual(game.nexusCards!.cards, previous.nexusCards!.cards); assert.deepEqual(game.traitorReserve, previous.traitorReserve); game = await act(f, game, auth.playerId, { type: 'setAutopilot', difficulty: null }); assert.equal(player(game, auth.playerId).autopilot ?? null, null); assert.deepEqual(game.pendingNexusHarkonnenBetrayal, f.initial.pendingNexusHarkonnenBetrayal); } await restored(f, game); });