Files
Dune/tests/coalition-truth-alliance-controls.test.tsx
T
Quintine 3e0fa2a8a2
Verify and publish checkpoint container / container (push) Successful in 11m22s
Honor group alliances in Truthtrance
2026-10-29 00:41:56 +11:00

93 lines
4.8 KiB
TypeScript

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]);
});