Files
Dune/tests/guild-ambassador-no-field-controls.test.tsx
T
Quintine 659e2a4a9a Support Basic No-Field Guild Ambassador shipments
Connect native concealment and allied supplier/recipient consent to the independent grant, with explicit provisional cap and transport-fee policy. Preserve actual tokens, ordinary allowances, source-scoped Karama prevention and original income/arrival continuation.

Add genuine three/four-seat sources, focused quote/control/engine cases and bounded runtime/minimal-bot proof. Advanced and optional profiles retain their existing guards.
2026-10-24 17:36:12 +11:00

251 lines
11 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { Children, createElement, isValidElement, type ReactNode } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import {
AmbassadorNoField,
AmbassadorNoFieldChoice,
type AmbassadorNoFieldOffer,
} from '../components/ambassador-no-field';
import { AmbassadorShipment } from '../components/ambassador-shipment';
import type { Action, GameView } from '../game/engine';
const event = 'guild-ambassador-no-field-original';
function option(
key: string,
fields: Partial<AmbassadorNoFieldOffer['options'][number]> = {},
): AmbassadorNoFieldOffer['options'][number] {
return {
key,
label: `Concealed No-Field ${key} · Arrakeen sector 10 · free`,
territory: 'arrakeen',
sector: 10,
action: {
type: 'decision', event, amount: 1, noField: `richese-${key}`,
territory: 'arrakeen', sector: 10,
},
blocked: null,
...fields,
};
}
function offer(stage: AmbassadorNoFieldOffer['stage'] = 'own'): AmbassadorNoFieldOffer {
return {
stage,
policy: 'Provisional Guild Ambassador No-Field policy: a concealed native marker counts as one force and costs no spice. This independent grant does not spend ordinary shipment or movement.',
options: [0, 3, 5].map((value) => option(String(value))),
};
}
// These component contracts deliberately use projected offers. The related
// engine source fixtures exercise actual token custody, payment and continuation.
function projected(noField: AmbassadorNoFieldOffer | null = offer()): GameView {
return {
me: 'r',
players: [
{ id: 'r', name: 'Richese', faction: 'richese', reserves: 10 },
{ id: 'e', name: 'Ecaz', faction: 'ecaz', reserves: 10 },
],
decision: { kind: 'ecazAmbassador', player: 'r', event },
ambassadorEntry: {
event, stage: 'ship', effect: 'guild', beneficiary: 'r', noField,
shipment: {
maximum: 4, eliteReserves: 0,
destinations: [{ territory: 'arrakeen', sector: 10, maximum: 4, blocked: null }],
},
},
} as unknown as GameView;
}
function markup(game: GameView, busy = false) {
return renderToStaticMarkup(createElement(AmbassadorShipment, {
game, busy, act() {},
}));
}
function click(node: ReactNode, selector: string): boolean {
for (const child of Children.toArray(node)) {
if (!isValidElement<{
children?: ReactNode;
onClick?: () => void;
'data-testid'?: string;
}>(child)) continue;
if (child.props['data-testid'] === selector && child.props.onClick) {
child.props.onClick();
return true;
}
if (click(child.props.children, selector)) return true;
}
return false;
}
function choose(
current: AmbassadorNoFieldOffer,
selectedKey: string,
busy = false,
decline = false,
): Action | undefined {
let action: Action | undefined;
const tree = AmbassadorNoFieldChoice({
offer: current, event, selectedKey, busy,
onSelect() {}, act(value) { action = value; },
});
click(tree, decline ? 'ambassador-no-field-decline' : 'ambassador-no-field-submit');
return action;
}
void test('native marker controls retain the separate physical grant and submit only the complete projected token action', () => {
const current = offer();
const html = markup(projected(current));
assert.match(html, /aria-label="Ambassador No-Field shipment"/);
assert.match(html, /Provisional Guild Ambassador No-Field policy/);
assert.match(html, /does not spend ordinary shipment or movement/);
for (const value of [0, 3, 5]) assert.match(html, new RegExp(`Concealed No-Field ${value}`));
assert.match(html, /Selected: 4 physical forces, including 0 elites/);
assert.match(html, /Ship selected forces for free/);
assert.match(html, /Send no forces/);
assert.doesNotMatch(html, /No-Field substitution.*unfinished/);
for (const selected of current.options) {
const action = choose(current, selected.key);
assert.deepEqual(action, selected.action);
assert.equal(action?.amount, 1);
assert.equal(action?.elite, undefined);
assert.equal(action?.smuggler, undefined);
assert.equal(action?.type, 'decision');
}
});
void test('zero reserves still expose marker destinations without inventing physical forces', () => {
const game = projected();
game.players[0].reserves = 0;
game.ambassadorEntry!.shipment!.maximum = 0;
game.ambassadorEntry!.shipment!.destinations = [];
const html = markup(game);
assert.match(html, /value="arrakeen" selected=""/);
assert.match(html, /value="arrakeen:10" selected=""/);
assert.match(html, /Concealed No-Field 0/);
assert.match(html, /Selected: 0 physical forces, including 0 elites/);
assert.match(html, /You have no reserve forces available for this shipment/);
assert.match(html, /Send no forces/);
});
void test('an existing native marker can be revealed without choosing or carrying a destination', () => {
const current = offer();
current.options = [option('reveal', {
label: 'Reveal existing No-Field', territory: undefined, sector: undefined,
action: { type: 'decision', event, revealNoField: true },
})];
const selection = AmbassadorNoField({
game: projected(current), busy: false, act() {}, destination: 'hagga_basin', sector: 12,
});
assert.ok(selection);
assert.equal(selection.props.offer.options.length, 1);
assert.deepEqual(choose(current, 'reveal'), current.options[0].action);
});
void test('allied request hides token values and both private decision stages replace the physical form', () => {
const request = offer('request');
request.policy = 'Request a whole-grant No-Field substitution from your actual Richese ally. The ally chooses its private marker before you consent.';
request.options = [option('request-arrakeen', {
label: 'Ask Richese for a No-Field in Arrakeen sector 10',
action: { type: 'decision', event, territory: 'arrakeen', sector: 10, requestNoField: true },
})];
const requestGame = projected(request);
requestGame.me = 'e';
requestGame.decision!.player = 'e';
requestGame.ambassadorEntry!.beneficiary = 'e';
const requestHtml = markup(requestGame);
assert.match(requestHtml, /Request an allied No-Field/);
assert.match(requestHtml, /Ask Richese for a No-Field in Arrakeen/);
assert.doesNotMatch(requestHtml, /No-Field [035]/);
assert.match(requestHtml, /Ship selected forces for free/);
assert.deepEqual(choose(request, request.options[0].key), request.options[0].action);
for (const stage of ['supply', 'consent'] as const) {
const current = offer(stage);
current.policy = 'The allied marker reveals immediately: 3 reserve forces, capped at four. Mandatory transport fee: 1 spice to the Guild or bank, paid by either ally; this does not spend ordinary shipment or movement.';
current.options = stage === 'supply'
? [0, 3, 5].flatMap((value) => ['r', 'e'].map((payer) => option(`${stage}-${value}-${payer}`, {
label: `Offer No-Field ${value} · ${Math.min(value, 4)} forces · ${payer === 'r' ? 'Richese' : 'Ecaz'} pays 1 spice`,
action: { type: 'decision', event, choice: stage, token: `richese-${value}`, payer },
})))
: [option(`${stage}-3`, {
label: 'Accept No-Field 3 · 3 forces · Richese pays 1 spice',
action: { type: 'decision', event, choice: stage, token: 'richese-3', payer: 'r' },
})];
current.decline = { type: 'decision', event, declineNoField: true };
current.declineLabel = 'Decline and return to the original grant';
const game = projected(current);
game.ambassadorEntry!.beneficiary = 'e';
game.ambassadorEntry!.shipment = null;
if (stage === 'consent') {
game.me = 'e';
game.decision!.player = 'e';
}
const html = markup(game);
assert.match(html, /aria-label="Ambassador No-Field shipment"/);
assert.match(html, /No-Field 3 · 3 forces · Richese pays 1 spice/);
if (stage === 'supply') {
assert.match(html, /No-Field 0 · 0 forces · Ecaz pays 1 spice/);
assert.match(html, /No-Field 5 · 4 forces · Ecaz pays 1 spice/);
}
assert.match(html, /Mandatory transport fee: 1 spice to the Guild or bank/);
assert.match(html, /Decline and return to the original grant/);
assert.doesNotMatch(html, /Physical forces to ship|Ship selected forces for free|Send no forces/);
for (const selected of current.options)
assert.deepEqual(choose(current, selected.key), selected.action);
assert.deepEqual(choose(current, '', false, true), current.decline);
}
});
void test('busy, unselected, blocked and obsolete choices cannot dispatch through their callbacks', () => {
const current = offer('supply');
current.options.push(option('blocked', { blocked: 'The payer cannot cover this transport fee.' }));
current.decline = { type: 'decision', event, declineNoField: true };
for (const selected of ['', 'obsolete', 'blocked']) assert.equal(choose(current, selected), undefined);
assert.equal(choose(current, '3', true), undefined);
assert.equal(choose(current, '', true, true), undefined);
const html = renderToStaticMarkup(createElement(AmbassadorNoFieldChoice, {
offer: current, event, selectedKey: '3', onSelect() {}, act() {}, busy: true,
}));
assert.equal((html.match(/<button\b[^>]*\sdisabled=""/g) ?? []).length, 2);
assert.match(html, /The payer cannot cover this transport fee/);
assert.equal(choose({ ...current, options: [] }, '3'), undefined);
});
void test('fresh event, stage and destination projections reset selection and use only the incoming authoritative action', () => {
const game = projected();
const first = AmbassadorNoField({ game, busy: false, act() {}, destination: 'arrakeen', sector: 10 });
assert.ok(first);
const next = structuredClone(game);
next.ambassadorEntry!.event = 'guild-ambassador-no-field-next';
next.ambassadorEntry!.noField!.options[1].action = { type: 'decision', event: 'guild-ambassador-no-field-next', amount: 1, noField: 'new-3', territory: 'arrakeen', sector: 10 };
const fresh = AmbassadorNoField({ game: next, busy: false, act() {}, destination: 'arrakeen', sector: 10 });
assert.ok(fresh);
assert.notEqual(first.key, fresh.key);
assert.deepEqual(choose(fresh.props.offer, '3'), next.ambassadorEntry!.noField!.options[1].action);
next.ambassadorEntry!.noField!.stage = 'supply';
const stage = AmbassadorNoField({ game: next, busy: false, act() {} });
assert.ok(stage);
assert.notEqual(fresh.key, stage.key);
const otherDestination = AmbassadorNoField({ game, busy: false, act() {}, destination: 'hagga_basin', sector: 12 });
assert.ok(otherDestination);
assert.notEqual(first.key, otherDestination.key);
assert.equal(otherDestination.props.offer.options.length, 0);
assert.equal(choose(otherDestination.props.offer, '3'), undefined);
});
void test('only the current Ambassador decision actor gets controls, even if an obsolete projection remains', () => {
const game = projected();
game.decision!.player = 'e';
assert.equal(markup(game), '');
assert.equal(AmbassadorNoField({ game, busy: false, act() {} }), null);
const ended = projected();
ended.ambassadorEntry!.stage = 'move';
assert.equal(markup(ended), '');
const noOffer = projected(null);
assert.doesNotMatch(markup(noOffer), /Ambassador No-Field shipment/);
assert.match(markup(noOffer), /Send no forces/);
});