import assert from 'node:assert/strict'; import test from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { TurnActionFields, turnActionInputError } from '../components/turn-action-fields'; import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance'; import { applyAction, createGame, newPlayer, viewGame, type GameView } from '../game/engine'; import type { MixedActionExpression } from '../game/mixed-action-question'; import type { TurnActionLeaf } from '../game/turn-action-promises'; const payment = (recipient: string): MixedActionExpression => ({kind:'action', action:{kind:'payment', recipient, minimum:1}}); /** Controlled quiet Nexus and preview ledger, with native pair and triple * formation. The editor consumes the resulting public projection. */ function projected(triple: boolean): GameView { let game = createGame('TURNPAYGROUP', 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('three-short'); game = applyAction(game, 'a', {type:'alliance', target:'b'}); game = applyAction(game, 'b', {type:'alliance', target:'a'}); if (triple) { const pair = coalitionOf(game.players, game.coalitionState, 'a'); assert.ok(pair); game = applyAction(game, 'a', {type:'coalitionPropose', groupId:pair.id, entrant:'c', expectedRevision:game.coalitionState!.revision}); for (const actor of ['b','c']) { const offer = game.coalitionState!.proposal!; game = applyAction(game, actor, {type:'coalitionAnswer', event:offer.event, nexus:offer.nexus, turn:offer.turn, accept:true, expectedRevision:game.coalitionState!.revision}); } } return viewGame(game, 'd'); } function html(game: GameView, recipient: string) { return renderToStaticMarkup(createElement(TurnActionFields, { game, target:'a', value:payment(recipient), disabled:false, onChange() {}, })); } void test('whole-turn payment editor excludes every group ally and accepts an outsider', () => { const game = projected(true); assert.equal(game.players.find(player => player.id === 'a')!.ally, null, 'Triple membership must come from the projected coalition ledger.'); for (const id of ['a','b','c']) { assert.match(turnActionInputError(payment(id), game, 'a') ?? '', /non-allied recipient/); assert.doesNotMatch(html(game, 'd'), new RegExp(`