import assert from 'node:assert/strict'; import test from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { AlliancePromiseGuideButton } from '../components/alliance-promises'; import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance'; import { applyAction, createGame, newPlayer, viewGame, type Action, type Game, type GameView } from '../game/engine'; const send = (game: Game, actor: string, action: Action) => applyAction(game, actor, action); /** Real public pair/group formation with controlled promise and guide * projections. These cases exercise client dispatch and stale guards. */ function projected(kind: 'yes' | 'no' | 'expression' | 'pair'): GameView { let game = createGame('GROUPTRUTHCONTROLS', newPlayer('a', 'Atreides', 'atreides'), false); game.players.push(newPlayer('b', 'Emperor', 'emperor'), newPlayer('c', 'Fremen', 'fremen'), newPlayer('d', 'Harkonnen', 'harkonnen')); Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true, order:['a','b','c','d'], ready:[]}); game.coalitionState = emptyCoalitionState('unlimited'); game = send(game, 'a', {type:'alliance', target:'b'}); game = send(game, 'b', {type:'alliance', target:'a'}); if (kind !== 'pair') { const original = coalitionOf(game.players, game.coalitionState, 'a'); assert.ok(original); game = send(game, 'a', {type:'coalitionPropose', groupId:original.id, entrant:'c', expectedRevision:game.coalitionState!.revision}); for (const actor of ['b','c']) { const offer = game.coalitionState!.proposal!; game = send(game, actor, {type:'coalitionAnswer', event:offer.event, nexus:offer.nexus, turn:offer.turn, accept:true, expectedRevision:game.coalitionState!.revision}); } } const view = viewGame(game, 'a'); view.alliancePromiseNexus = {turn:2}; view.alliancePromises = kind === 'expression' ? [] : [{event:'controlled', player:'a', asker:'d', turn:2, partner:'b', answer:kind !== 'no'}]; view.allianceExpressionPromises = kind !== 'expression' ? [] : [{event:'controlled', player:'a', asker:'d', turn:2, claim:{kind:'and', terms:[{kind:'partner',partner:'b'}, {kind:'partner',partner:'c'}]}, answer:true}]; view.alliancePromiseGuide = {action:kind === 'no' ? {type:'coalitionLeave', expectedRevision:view.coalitionState!.revision, alliancePromiseEvent:'controlled'} : {type:'ready', alliancePromiseEvent:'controlled'}}; return view; } function button(game: GameView, sent: Action[], busy = false) { const result = AlliancePromiseGuideButton({game, busy, send(action) { sent.push(action); }}); assert.ok(result); return result; } void test('group Yes and whole-AND guides render an enabled Ready action', () => { for (const kind of ['yes', 'expression'] as const) { const game = projected(kind), sent:Action[] = []; const control = button(game, sent); assert.equal(control.props.disabled, false); assert.match(renderToStaticMarkup(createElement(AlliancePromiseGuideButton, {game, busy:false, send() {}})), /Confirm Ready for this Nexus/); control.props.onClick(); assert.deepEqual(sent, [game.alliancePromiseGuide!.action]); } }); void test('group No guide renders and dispatches its exact event-bound leave action', () => { const game = projected('no'), sent:Action[] = []; const control = button(game, sent); assert.equal(control.props.disabled, false); assert.match(renderToStaticMarkup(createElement(AlliancePromiseGuideButton, {game, busy:false, send() {}})), /Leave your group alliance while honoring your answers/); control.props.onClick(); assert.deepEqual(sent, [game.alliancePromiseGuide!.action]); }); void test('group guide refuses stale revision, membership, event, and quiet-window changes', () => { const mutations: ((game: GameView) => void)[] = [ game => { game.coalitionState!.revision += 1; }, game => { game.coalitionState!.groups[0].members.pop(); }, game => { game.players = game.players.filter(player => player.id !== 'b'); }, game => { game.alliancePromises[0].event = 'changed'; }, game => { game.alliancePromiseGuide!.action.expectedRevision = -1; }, game => { game.ready.push('a'); }, game => { game.nexus = false; }, game => { game.automaticContinuationPending = true; }, ]; for (const mutate of mutations) { const game = projected('no'), sent:Action[] = [], control = button(game, sent); mutate(game); control.props.onClick(); assert.deepEqual(sent, []); } }); void test('reciprocal pair guidance remains usable with a staged coalition ledger', () => { const game = projected('pair'), sent:Action[] = []; const control = button(game, sent); assert.equal(control.props.disabled, false); control.props.onClick(); assert.deepEqual(sent, [game.alliancePromiseGuide!.action]); });