import test from 'node:test'; import assert from 'node:assert/strict'; import { createGame, newPlayer, type Game } from '../game/engine'; import { createHomeworldCustody, HomeworldCustodyError } from '../game/homeworld-custody'; import { homeworldContext } from '../game/homeworld-game'; import { appendHomeworldArrivalAmbassador, homeworldArrivalSignature } from '../game/homeworld-arrival'; import { makeHomeworldRevivalReturn, makeHomeworldRevivalProgress, makeSouthernRevivalAllocation, currentHomeworldRevivalArrival, openSouthernRevivalChoice, commitSouthernRevivalChoice, completeSouthernRevivalChild } from '../game/homeworld-revival-return'; import { terrorEntrySignature, validateTerrorEntrySignature } from '../game/terror-entry-receipt'; import { revivalOwnsTerrorEntry, validateRevivalTerrorEntry, validateRevivalArrivalSource, validateRevivalTerrorPacket, validRevivalEntryParent, validRevivalTerrorPacket, type RevivalEntryParent, type RevivalTerrorPacket, type RevivalArrivalDetails, } from '../game/revival-terror-entry'; type Entry = NonNullable; /** Controlled conserved arrival context with a real constructed original-return * receipt. No native play or card-disposal chronology is claimed here. */ function fixture(fremen = false) { const g = createGame('REVIVALTERRORSOURCE', newPlayer('native', 'Native', fremen ? 'fremen' : 'tleilaxu'), false, ['ix', 'ecaz']); g.players.push(newPlayer('bg', 'BG', 'beneGesserit'), newPlayer('m', 'Moritani', 'moritani'), newPlayer('ec', 'Ecaz', 'ecaz')); g.status = 'playing'; g.turn = 2; g.phase = 4; const native = g.players[0]; native.reserves = fremen ? 2 : 10; native.tanks = fremen ? 17 : 8; native.forces = { 'arrakeen:10': fremen ? 1 : 2 }; if (fremen) native.elites = { reserves: 1, tanks: 1, forces: { 'arrakeen:10': 1 }, revived: 1 }; g.homeworlds = { custody: createHomeworldCustody(homeworldContext(g)) }; const frame = makeHomeworldRevivalReturn({ event: 'original-return', turn: 2, phase: 4, player: 'native', source: 'normal', group: fremen ? { amount: 1, elite: 1, free: 1 } : { amount: 3, elite: 0, free: 2 }, quote: fremen ? { kind: 'fedaykin', normal: 0, elite: 1, beforePopulation: 2, afterPopulation: 3, blocked: null } : { kind: 'tleilax', normal: 2, elite: 0, beforePopulation: 9, afterPopulation: 12, blocked: null } }); frame.stage = 'arrival'; frame.destination = 'arrakeen:10'; frame.resumeResponse = null; frame.resumeSignature = 'null'; frame.ambassadors = []; frame.arrivalSignature = homeworldArrivalSignature(frame); g.homeworldRevivalReturn = frame; g.homeworldRevivalProgress = makeHomeworldRevivalProgress(frame); const entry: Entry = { token: 'original-opaque-token', entrant: 'native', territory: 'arrakeen', sector: 10, amount: frame.quote.normal + frame.quote.elite, elite: frame.quote.elite, cause: 'homeworldRevival', turn: 2, phase: 4, stage: 'offer', resume: 'none', revivalArrival: { event: frame.event } }; entry.entrySignature = terrorEntrySignature(entry); return { g, frame, entry }; } void test('Southern live Terror source binds each actual child subset and never the whole root or a retired child', () => { const { g, entry } = fixture(true); g.players[0].reserves = 3; g.players[0].elites!.reserves = 2; const root = makeSouthernRevivalAllocation({ event: 'southern-root', turn: 2, phase: 4, player: 'native', source: 'normal', group: { amount: 2, elite: 2, free: 2 }, quote: { kind: 'fedaykin', normal: 0, elite: 2, beforePopulation: 1, afterPopulation: 3, blocked: null } }); root.resumeResponse = null; root.resumeSignature = 'null'; openSouthernRevivalChoice(root, 'southern-child-1'); commitSouthernRevivalChoice(root, 'southern-child-1', { amount: 1, destination: 'arrakeen:10' }); g.homeworldRevivalReturn = root; g.homeworldRevivalProgress = makeHomeworldRevivalProgress(root); Object.assign(entry, { amount: 1, elite: 1, revivalArrival: { event: 'southern-child-1' } }); entry.entrySignature = terrorEntrySignature(entry); validateRevivalTerrorEntry(g, entry); assert.throws(() => validateRevivalArrivalSource(g, { event: root.event }, entry), HomeworldCustodyError); assert.throws(() => validateRevivalArrivalSource(g, entry.revivalArrival!, { ...entry, amount: 2, elite: 2 }), HomeworldCustodyError); const historical = packet(entry); validateRevivalTerrorPacket({ ...g, revivalPlayer: root.player }, historical, 'arrakeen:10', 'southern-child-1'); completeSouthernRevivalChild(root, 'southern-child-1', 'southern-child-2'); commitSouthernRevivalChoice(root, 'southern-child-2', { amount: 1, destination: 'polar_sink:0' }); g.homeworldRevivalProgress = makeHomeworldRevivalProgress(root); assert.equal(currentHomeworldRevivalArrival(root)!.event, 'southern-child-2'); assert.throws(() => validateRevivalTerrorEntry(g, entry), HomeworldCustodyError); // The old captured history remains historical without reopening its live source. validateRevivalTerrorPacket({ ...g, revivalPlayer: root.player }, historical, 'arrakeen:10', 'southern-child-1'); validateRevivalArrivalSource(g, { event: 'southern-child-2' }, { entrant: 'native', territory: 'polar_sink', sector: 0, amount: 1, elite: 1, cause: 'homeworldRevival' }); }); function spiritual(parent: RevivalEntryParent, territory = 'arrakeen', sector = 10): RevivalEntryParent { return { ...structuredClone(parent), spiritual: { event: 'original-spiritual-packet', owner: 'bg', turn: 2, phase: 4, territory, sector, amount: 1, revivalEvent: parent.event } }; } function packet(entry: Entry): RevivalTerrorPacket { return { event: entry.entrySignature!, owner: 'm', territory: entry.territory, sector: entry.sector, amount: 2, elite: 0 }; } const sneakDetails = (source: RevivalTerrorPacket): RevivalArrivalDetails => ({ entrant: source.owner, territory: source.territory, sector: source.sector, amount: source.amount, elite: source.elite, cause: 'shipment' }); void test('original native return binds exact force types, clock and board destination without mutation', () => { for (const fremen of [false, true]) { const { g, entry } = fixture(fremen), before = structuredClone({ g, entry }); assert.equal(revivalOwnsTerrorEntry(g, entry), true); validateRevivalTerrorEntry(g, entry); validateTerrorEntrySignature(entry); assert.deepEqual({ g, entry }, before); } }); void test('present metadata rejects stale, foreign, wrong-actor and changed original groups', () => { const changes: ((g: Game, entry: Entry) => void)[] = [ (_g, entry) => { entry.revivalArrival!.event = 'foreign-return'; }, (_g, entry) => { entry.entrant = 'bg'; }, (_g, entry) => { entry.cause = 'movement'; }, (_g, entry) => { entry.amount++; }, (_g, entry) => { entry.elite = 1; }, (_g, entry) => { entry.territory = 'carthag'; entry.sector = 11; }, (_g, entry) => { entry.turn = 3; }, (_g, entry) => { entry.phase = 5; }, g => { delete g.homeworldRevivalReturn; }, g => { g.homeworldRevivalReturn!.stage = 'complete'; }, g => { g.homeworldRevivalReturn!.group.amount++; }, ]; for (const change of changes) { const { g, entry } = fixture(); change(g, entry); const before = structuredClone({ g, entry }); assert.throws(() => revivalOwnsTerrorEntry(g, entry), HomeworldCustodyError); assert.deepEqual({ g, entry }, before); } }); void test('Spiritual entry uses the exact committed BG packet rather than original native counts or current survivors', () => { const { g, entry } = fixture(); entry.revivalArrival = spiritual(entry.revivalArrival!); entry.entrant = 'bg'; entry.cause = 'advisor'; entry.amount = 1; entry.entrySignature = terrorEntrySignature(entry); validateRevivalTerrorEntry(g, entry); assert.deepEqual(g.players.find(player => player.id === 'bg')!.forces, {}); for (const patch of [{ entrant: 'native' }, { cause: 'shipment' }, { amount: 2 }, { elite: 1 }]) assert.throws(() => validateRevivalTerrorEntry(g, { ...entry, ...patch } as Entry), HomeworldCustodyError); }); void test('Sneak source retains its captured exact revival identity after the original Terror action is gone', () => { const { g, entry, frame } = fixture(), terror = packet(entry); const parent = { event: frame.event, terror }; assert.ok(validRevivalTerrorPacket(terror)); validateRevivalTerrorPacket({ ...g, revivalPlayer: frame.player }, terror, frame.destination!, frame.event); validateRevivalArrivalSource(g, parent, sneakDetails(terror)); const changed = structuredClone(terror); changed.elite = 1; assert.equal(validRevivalTerrorPacket(changed), false); assert.throws(() => validateRevivalArrivalSource(g, { event: frame.event, terror: changed }, sneakDetails(changed)), HomeworldCustodyError); assert.throws(() => validateRevivalTerrorPacket({ ...g, revivalPlayer: frame.player }, terror, frame.destination!, 'foreign-return'), HomeworldCustodyError); assert.throws(() => validateRevivalArrivalSource(g, parent, { ...sneakDetails(terror), amount: 1 }), HomeworldCustodyError); assert.throws(() => validateRevivalArrivalSource(g, { event: frame.event, terror: { ...terror, owner: 'bg' } }, sneakDetails(terror)), HomeworldCustodyError); }); void test('captured BG Polar Terror source and subsequent Southern-derived Spiritual packet retain both actual packets', () => { const { g, entry, frame } = fixture(true); assert.throws(() => validateRevivalArrivalSource(g, spiritual({ event: frame.event }), { entrant: 'bg', territory: 'arrakeen', sector: 10, amount: 1, elite: 0, cause: 'advisor' }), HomeworldCustodyError); // A committed BG packet from a native T return can reach Polar Nexus Terror; // the resulting Sneak record binds that source point, not the revival sector. const t = fixture(); t.entry.revivalArrival = spiritual(t.entry.revivalArrival!, 'polar_sink', 0); Object.assign(t.entry, { entrant: 'bg', territory: 'polar_sink', sector: 0, amount: 1, elite: 0, cause: 'advisor' }); t.entry.nexusPlacements = { [t.entry.token]: 'original-nexus-placement' }; t.entry.entrySignature = terrorEntrySignature(t.entry); validateTerrorEntrySignature(t.entry); const terror = packet(t.entry); validateRevivalTerrorPacket({ ...t.g, revivalPlayer: t.frame.player }, terror, t.frame.destination!, t.frame.event); const southernTerror = packet(entry), combined = spiritual({ event: frame.event, terror: southernTerror }, 'polar_sink', 0); const details: RevivalArrivalDetails = { entrant: 'bg', territory: 'polar_sink', sector: 0, amount: 1, elite: 0, cause: 'advisor' }; const before = structuredClone({ g, combined, details }); validateRevivalArrivalSource(g, combined, details); assert.deepEqual({ g, combined, details }, before); assert.throws(() => validateRevivalArrivalSource(g, combined, { ...details, amount: 2 }), HomeworldCustodyError); }); void test('an owned Ambassador suspended for its physical discard still validates the original return', () => { const { g, entry, frame } = fixture(); const ambassador: NonNullable = { event: 'original-ambassador', owner: 'ec', entrant: 'native', territory: 'arrakeen', sector: 10, turn: 2, phase: 4, revivalEvent: frame.event, token: 'original-ambassador-token', stage: 'offer', copyChoices: [], resume: 'none' }; appendHomeworldArrivalAmbassador(frame, ambassador); // Only the wrapper is controlled: this unit does not claim a real discard. g.pendingTreacheryDiscard = { continuation: { kind: 'ambassador', entry: ambassador } } as Game['pendingTreacheryDiscard']; g.pendingAmbassador = null; const before = structuredClone(g); validateRevivalTerrorEntry(g, entry); assert.deepEqual(g, before); }); void test('source-absent legacy entries retain their exact old bytes and never gain revival ownership', () => { const { g, entry } = fixture(); delete entry.revivalArrival; entry.cause = 'shipment'; entry.entrySignature = terrorEntrySignature(entry); assert.equal(entry.entrySignature, JSON.stringify([entry.token, entry.entrant, entry.territory, entry.sector, entry.amount, entry.elite, entry.cause, entry.turn, entry.phase, entry.resume, null])); validateTerrorEntrySignature(entry); assert.equal(revivalOwnsTerrorEntry(g, entry), false); const noReceipt = { ...entry }; delete noReceipt.entrySignature; validateRevivalTerrorEntry(g, noReceipt); validateTerrorEntrySignature(noReceipt); assert.equal(revivalOwnsTerrorEntry(g, noReceipt), false); }); void test('new metadata is signature-bound and malformed optional packets cannot bypass receipt checks', () => { const { entry } = fixture(), original = entry.entrySignature; entry.revivalArrival = spiritual(entry.revivalArrival!); assert.notEqual(terrorEntrySignature(entry), original); assert.throws(() => validateTerrorEntrySignature(entry), HomeworldCustodyError); assert.equal(validRevivalEntryParent({ event: 'original-return', extra: true }), false); assert.equal(validRevivalEntryParent({ event: 'original-return', terror: { event: 'not-json' } }), false); assert.equal(validRevivalEntryParent({ event: 'original-return', spiritual: undefined }), false); delete entry.entrySignature; assert.throws(() => validateTerrorEntrySignature(entry), HomeworldCustodyError); });