import test from 'node:test'; import assert from 'node:assert/strict'; import type { RoomsClock } from '../db/rooms'; import { nexusPlayer } from './fixture-nexus-cards'; import { fremenBetrayalFixture, fremenBetrayalMovement, fremenBetrayalNextWorm, fremenBetrayalWormFixture, useLegacyFremenMovementReceipt, } from './fixture-nexus-fremen-betrayal'; import { unitStore } from './fixture-nexus-room-store'; const clock: RoomsClock = { now: () => 45000, sleep: async () => {} }; void test('concurrent authenticated Fremen Betrayal commits one physical card and restores the turn-long range limit', async t => { const store = unitStore(); t.after(() => store.sqlite.close()); const created = await store.rooms.createRoom('Fremen SQL', 'fremen', false, []); const code = created.view.code; const atreides = await store.rooms.joinRoom(code, 'Atreides', 'atreides'); const harkonnen = await store.rooms.joinRoom(code, 'Harkonnen', 'harkonnen'); const tokens = [created.token, atreides.token!, harkonnen.token!]; const auth = await Promise.all(tokens.map(token => store.rooms.authenticate(code, token))); const [fremen, ally, holder] = auth.map(seat => seat.playerId) as [string, string, string]; const { g, play } = fremenBetrayalFixture(false, [fremen, ally, holder]); g.code = code; g.version = (await store.rooms.readRoom(code)).version; store.sqlite.prepare('UPDATE rooms SET state = ?, version = ? WHERE code = ?') .run(JSON.stringify(g), g.version, code); store.writes.length = 0; const before = store.sqlite.prepare('SELECT state, version FROM rooms WHERE code = ?').get(code); const rivalView = await store.rooms.readSeatView(code, auth[1]); assert.equal(rivalView.nexusFremenBetrayal, null); assert.equal(rivalView.nexusCards?.card, null); const results = await Promise.allSettled([ store.restart().act(code, auth[2], g.version, play, clock), store.restart().act(code, auth[2], g.version, play, clock), ]); assert.equal(results.filter(result => result.status === 'fulfilled').length, 1); assert.equal(results.filter(result => result.status === 'rejected').length, 1); assert.notDeepEqual(store.sqlite.prepare('SELECT state, version FROM rooms WHERE code = ?').get(code), before); const saved = await store.restart().readRoom(code); assert.equal(saved.nexusCards!.cards!.hands[holder], null); assert.equal(saved.nexusCards!.cards!.discard.filter(card => card === 'fremen').length, 1); assert.equal(saved.nexusFremenBetrayalHistory?.length, 1); assert.equal(nexusPlayer(saved, fremen).fremenNexusMovementBlockedTurn, saved.turn); const freshAuth = await store.restart().authenticate(code, tokens[0]); const view = await store.restart().readSeatView(code, freshAuth); assert.equal(view.players.find(player => player.id === fremen)!.fremenMovementBlocked, true); assert.equal(view.nexusFremenBetrayal, null); const stable = store.sqlite.prepare('SELECT state, version FROM rooms WHERE code = ?').get(code); await assert.rejects(store.restart().act(code, freshAuth, saved.version, fremenBetrayalMovement(2), clock)); await assert.rejects(store.restart().act(code, auth[2], saved.version, play, clock)); assert.deepEqual(store.sqlite.prepare('SELECT state, version FROM rooms WHERE code = ?').get(code), stable); const legacy = await store.restart().readRoom(code); useLegacyFremenMovementReceipt(legacy); store.sqlite.prepare('UPDATE rooms SET state = ?, version = ? WHERE code = ?') .run(JSON.stringify(legacy), legacy.version, code); const oldRow = store.sqlite.prepare('SELECT state, version FROM rooms WHERE code = ?').get(code); const recoveredView = await store.restart().readSeatView(code, freshAuth); assert.equal(recoveredView.players.find(player => player.id === fremen)!.fremenMovementBlocked, true); assert.deepEqual(store.sqlite.prepare('SELECT state, version FROM rooms WHERE code = ?').get(code), oldRow); await store.restart().act(code, freshAuth, legacy.version, fremenBetrayalMovement(1), clock); const progressed = await store.restart().readRoom(code); assert.equal(nexusPlayer(progressed, fremen).moved, 1); }); void test('pre-blow Fremen Betrayal survives concurrent SQL spend and an actual worm without a ride', async t => { const store = unitStore(); t.after(() => store.sqlite.close()); const created = await store.rooms.createRoom('Fremen Worm SQL', 'fremen', false, []); const code = created.view.code; const atreides = await store.rooms.joinRoom(code, 'Atreides', 'atreides'); const harkonnen = await store.rooms.joinRoom(code, 'Harkonnen', 'harkonnen'); const tokens = [created.token, atreides.token!, harkonnen.token!]; const auth = await Promise.all(tokens.map(token => store.rooms.authenticate(code, token))); const ids = auth.map(seat => seat.playerId) as [string, string, string]; const [fremen, , holder] = ids; const { g, play } = fremenBetrayalWormFixture(false, ids); fremenBetrayalNextWorm(g, 'red_chasm'); g.code = code; g.version = (await store.rooms.readRoom(code)).version; store.sqlite.prepare('UPDATE rooms SET state = ?, version = ? WHERE code = ?') .run(JSON.stringify(g), g.version, code); store.writes.length = 0; assert.equal((await store.rooms.readSeatView(code, auth[0])).nexusFremenBetrayal, null); assert.equal((await store.rooms.readSeatView(code, auth[2])).nexusFremenBetrayal?.mode, 'worm'); const outcomes = await Promise.allSettled([ store.restart().act(code, auth[2], g.version, play, clock), store.restart().act(code, auth[2], g.version, play, clock), ]); assert.equal(outcomes.filter(result => result.status === 'fulfilled').length, 1); assert.equal(outcomes.filter(result => result.status === 'rejected').length, 1); let saved = await store.restart().readRoom(code); assert.equal(saved.nexusFremenBetrayalHistory?.[0].mode, 'worm'); assert.equal(nexusPlayer(saved, fremen).fremenNexusWormBlockedTurn, saved.turn); assert.equal(saved.nexusCards!.cards!.discard.filter(card => card === 'fremen').length, 1); for (let i = 0; i < auth.length; i++) { if (!saved.ready.includes(auth[i].playerId)) { await store.restart().act(code, auth[i], saved.version, { type: 'ready' }, clock); saved = await store.restart().readRoom(code); } } for (let step = 0; saved.response && step < 8; step++) { const id = saved.players.find(player => !saved.response!.passed.includes(player.id))!.id; const index = auth.findIndex(seat => seat.playerId === id); await store.restart().act(code, auth[index], saved.version, { type: 'passResponse' }, clock); saved = await store.restart().readRoom(code); } assert.equal(saved.nexus, true); assert.deepEqual(saved.wormRides, []); assert.notEqual(saved.decision?.kind, 'wormRide'); assert.equal(saved.nexusFremenCunningOffer ?? null, null); assert.equal(nexusPlayer(saved, fremen).forces['red_chasm:7'], 4); for (const token of tokens) { const seat = await store.restart().authenticate(code, token); const view = await store.restart().readSeatView(code, seat); assert.equal(view.players.find(player => player.id === fremen)!.fremenNexusWormBlocked, true); assert.equal(view.nexusFremenBetrayal, null); } const before = store.sqlite.prepare('SELECT state, version FROM rooms WHERE code = ?').get(code); await assert.rejects(store.restart().act(code, auth[2], saved.version, play, clock)); assert.deepEqual(store.sqlite.prepare('SELECT state, version FROM rooms WHERE code = ?').get(code), before); assert.equal(saved.nexusCards!.cards!.hands[holder], null); });