Support group travel agreements
Verify and publish checkpoint container / container (push) Successful in 11m13s

This commit is contained in:
Quintine committed 2026-10-29 08:04:30 +11:00
1 parent 82d857b667
commit e1e02fc1a1
10 files changed
+432 -56

No files matched your search

@@ -0,0 +1,187 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { actTruthMovement as act, truthMovementClean as clean, truthMovementInventory as inventory,
truthMovementPlayer as player } from './fixture-truth-movement';
import { nextStrongholdFactionsNativeStep as next } from './fixture-stronghold-factions';
const proposer = 'atreides', traveler = 'emperor', first = 'harkonnen', second = 'fremen';
const shipment: Action = {type:'ship', territory:'polar_sink', sector:0, amount:1, elite:0, allyPayment:0};
const movement: Action = {type:'move', from:'polar_sink:0', territory:'imperial_basin', sector:9, amount:1};
type Term = {actor:string; kind:'ship'|'move'; territory:string; minimum:number; answer:boolean};
function reach(game: Game, until: (game: Game) => boolean): Game {
for (let count = 0; count < 1200; count++) {
if (until(game)) return game;
assert.notEqual(game.status, 'finished', 'The native game ended before the travel opportunity.');
const step = next(game); assert.ok(step, `Supply native continuation at ${game.turn}/${game.phase}`);
game = act(game, step.actor, step.action);
}
throw Error('The native travel opportunity was not reached.');
}
function settle(game: Game): Game {
return reach(game, state => clean(state) && !state.pendingShipment);
}
/** Four actual human Basic seats. Reorder only original unopened starting
* cards and Spice cards, and use native setup and phase actions to reach the
* Emperor's unused first-turn shipment. No resource, force, promise, vote or
* pending decision is installed by the test. */
function nativeSource(): Game {
let game = createGame('GROUPTRAVEL', newPlayer(proposer, 'Atreides', 'atreides'), false, []);
for (const [id, faction] of [[traveler,'emperor'], [first,'harkonnen'], [second,'fremen']] as const)
joinGame(game, newPlayer(id, id, faction));
for (const seat of game.players) game = act(game, seat.id, {type:'ready'});
game = act(game, proposer, {type:'start'});
assert.equal(game.status, 'setup');
const cards = game.deck.filter(card => ['projectile','poison','shield','snooper'].includes(card.kind)).slice(0, 4);
assert.equal(cards.length, 4);
for (const [position, card] of cards.entries()) {
const from = game.deck.findIndex(row => row.id === card.id); assert.ok(from >= position);
[game.deck[position], game.deck[from]] = [game.deck[from], game.deck[position]];
}
game = reach(game, state => state.status === 'playing');
assert.equal(game.status, 'playing');
const land = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery); assert.ok(land >= 0);
game.spiceDeck.unshift(game.spiceDeck.splice(land, 1)[0]);
game = reach(game, state => state.turn === 1 && state.phase === 5 && state.active === traveler && clean(state));
assert.equal(player(game, traveler).shipped, false);
assert.ok(player(game, traveler).spice >= 2);
assert.ok(!game.travelPromises?.length);
return game;
}
let source: Game | undefined;
const fixture = () => structuredClone(source ??= nativeSource());
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => act(game, actor, action));
assert.deepEqual(game, before, 'Rejected deal and travel actions preserve the table.');
}
function offer(game: Game, targets: string[], travel: Term) {
const nextGame = act(game, proposer, {type:'dealOffer', targets, terms:'A shared current-turn journey.', travel});
const deal = nextGame.deals?.at(-1); assert.ok(deal);
return {game:nextGame, id:deal.id};
}
function respond(game: Game, actor: string, id: string, accept = true) {
return act(game, actor, {type:'dealRespond', deal:id, accept});
}
function status(game: Game, actor: string, id: string) {
return viewGame(game, actor).dealTravelStatus[id];
}
function receipt(game: Game, id: string) {
const row = game.travelPromises?.find(promise => promise.sourceDeal === id); assert.ok(row);
return row;
}
void test('a named traveler accepts last and performs the real shipment for a three-party deal', () => {
const initial = fixture(), originalCards = inventory(initial);
const offered = offer(initial, [first, traveler],
{actor:traveler, kind:'ship', territory:'polar_sink', minimum:1, answer:true});
let game = offered.game;
assert.equal(game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
reject(game, traveler, {type:'dealRespond', deal:offered.id, accept:true});
game = respond(game, first, offered.id);
assert.equal(game.deals!.at(-1)!.status, 'offered');
assert.equal(game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
game = respond(game, traveler, offered.id);
assert.equal(game.deals!.at(-1)!.status, 'active');
assert.equal(receipt(game, offered.id).player, traveler);
assert.equal(receipt(game, offered.id).asker, proposer);
for (const viewer of [proposer, first, traveler, second]) assert.equal(status(game, viewer, offered.id), 'pending');
reject(game, traveler, {type:'endMovement'});
const completed = settle(act(game, traveler, shipment));
assert.equal(player(completed, traveler).forces['polar_sink:0'], 1);
assert.equal(player(completed, traveler).reserves, player(initial, traveler).reserves - 1);
for (const viewer of [proposer, first, traveler]) assert.equal(status(completed, viewer, offered.id), 'fulfilled');
assert.equal(completed.travelPromises!.filter(row => row.sourceDeal === offered.id).length, 1);
assert.deepEqual(inventory(completed), originalCards);
});
void test('four-party consent requires all other recipients first and release requires every participant', () => {
const offered = offer(fixture(), [traveler, first, second],
{actor:traveler, kind:'ship', territory:'polar_sink', minimum:1, answer:true});
let game = offered.game;
game = respond(game, second, offered.id);
reject(game, traveler, {type:'dealRespond', deal:offered.id, accept:true});
game = respond(game, first, offered.id);
game = respond(game, traveler, offered.id);
assert.equal(game.deals!.at(-1)!.status, 'active');
for (const actor of [proposer, second, first]) {
game = act(game, actor, {type:'dealRelease', deal:offered.id, agree:true});
assert.equal(status(game, traveler, offered.id), 'pending');
}
reject(game, traveler, {type:'endMovement'});
game = act(game, traveler, {type:'dealRelease', deal:offered.id, agree:true});
assert.equal(status(game, traveler, offered.id), 'released');
assert.equal(receipt(game, offered.id).releasedByDeal, true);
game = settle(act(game, traveler, {type:'endMovement'}));
assert.equal(status(game, first, offered.id), 'released');
});
for (const answer of [true, false]) void test(`group movement ${answer ? 'requires' : 'forbids'} one real matching action`, () => {
const moved = settle(act(fixture(), traveler, shipment));
const offered = offer(moved, [first, traveler],
{actor:traveler, kind:'move', territory:'imperial_basin', minimum:1, answer});
let game = respond(offered.game, first, offered.id);
game = respond(game, traveler, offered.id);
if (answer) {
reject(game, traveler, {type:'endMovement'});
game = settle(act(game, traveler, movement));
assert.equal(player(game, traveler).forces['imperial_basin:9'], 1);
} else {
reject(game, traveler, movement);
game = settle(act(game, traveler, {type:'endMovement'}));
assert.equal(player(game, traveler).forces['imperial_basin:9'] ?? 0, 0);
}
assert.equal(status(game, first, offered.id), 'fulfilled');
});
void test('the traveler may decline early; malformed or uninvited actors and payment combinations reject', () => {
const initial = fixture(), term:Term = {actor:traveler, kind:'ship', territory:'polar_sink', minimum:1, answer:true};
for (const bad of [proposer, second, 'unknown'])
reject(initial, proposer, {type:'dealOffer', targets:[first, traveler], terms:'Terms', travel:{...term, actor:bad}});
reject(initial, proposer, {type:'dealOffer', targets:[first, traveler], terms:'Terms', travel:term,
payment:{kind:'immediateBribe', payer:proposer, recipient:traveler, amount:1}});
const offered = offer(initial, [first, traveler], term);
const declined = respond(offered.game, traveler, offered.id, false);
assert.equal(declined.deals!.at(-1)!.status, 'declined');
assert.equal(declined.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
});
void test('an unavailable journey rejects only the final traveler consent and leaves the group offer open', () => {
const alreadyShipped = settle(act(fixture(), traveler, shipment));
const offered = offer(alreadyShipped, [first, traveler],
{actor:traveler, kind:'ship', territory:'polar_sink', minimum:1, answer:true});
const waiting = respond(offered.game, first, offered.id);
assert.equal(waiting.deals!.at(-1)!.status, 'offered');
assert.equal(waiting.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
reject(waiting, traveler, {type:'dealRespond', deal:offered.id, accept:true});
assert.equal(viewGame(waiting, first).deals.at(-1)!.status, 'offered');
});
void test('the legacy two-person packet still binds the recipient without a named actor', () => {
const game = fixture();
let nextGame = act(game, proposer, {type:'dealOffer', target:traveler, terms:'Two-player travel.',
travel:{kind:'ship', territory:'polar_sink', minimum:1, answer:true}});
const deal = nextGame.deals!.at(-1)!;
nextGame = respond(nextGame, traveler, deal.id);
assert.equal(receipt(nextGame, deal.id).player, traveler);
assert.equal(status(nextGame, first, deal.id), 'pending');
});
void test('all minimal autopilot profiles can complete the named group travel obligation', () => {
const offered = offer(fixture(), [first, traveler],
{actor:traveler, kind:'ship', territory:'polar_sink', minimum:1, answer:true});
const accepted = respond(respond(offered.game, first, offered.id), traveler, offered.id);
for (const difficulty of DIFFICULTIES) {
let game = act(accepted, traveler, {type:'setAutopilot', difficulty});
for (let step = 0; step < 8 && status(game, traveler, offered.id) === 'pending'; step++) {
const actions = botActions(viewGame(game, traveler));
assert.ok(actions.length, `${difficulty} needs a legal native continuation`);
game = settle(act(game, traveler, actions[0]));
}
assert.equal(status(game, traveler, offered.id), 'fulfilled', difficulty);
}
});
+67 -5
View File
@@ -30,13 +30,19 @@ const base = {status:'playing', turn:2, me:'b', roomControl:null, coalitionState
const markup = (game:GameView, disabled = false) =>
renderToStaticMarkup(createElement(PublicDealList, {game, act() {}, disabled}));
void test('the offer packet retains the original recipient action and validates one exact physical term', () => {
void test('the offer packet retains legacy two-party terms and validates an explicitly named group traveler', () => {
assert.deepEqual(publicDealOfferAction(['b'], ' Terms ', undefined, 'a', 2, travel),
{type:'dealOffer', target:'b', terms:'Terms', travel});
const avoid = {...travel, kind:'move' as const, answer:false, minimum:20};
assert.deepEqual(publicDealOfferAction(['b'], 'Avoid', undefined, 'a', 2, avoid),
{type:'dealOffer', target:'b', terms:'Avoid', travel:avoid});
assert.equal(publicDealOfferAction(['b','c'], 'Terms', undefined, 'a', 2, travel), null);
assert.deepEqual(publicDealOfferAction(['b','c'], 'Terms', undefined, 'a', 2, {...travel, actor:'c'}),
{type:'dealOffer', targets:['b','c'], terms:'Terms', travel:{...travel, actor:'c'}});
assert.equal(publicDealOfferAction(['b','c'], 'Terms', undefined, 'a', 2, {...travel, actor:'a'}), null);
assert.equal(publicDealOfferAction(['b','c'], 'Terms', undefined, 'a', 2, {...travel, actor:'d'}), null);
assert.equal(publicDealOfferAction(['b'], 'Terms', undefined, 'a', 2, {...travel, actor:'b'}), null);
assert.equal(publicDealOfferAction(['b','c'], 'Terms', payment, 'a', 2, {...travel, actor:'c'}), null);
assert.deepEqual(publicDealOfferAction(['b'], 'Terms', payment, 'a', 2, travel),
{type:'dealOffer', target:'b', terms:'Terms', payment, travel});
assert.equal(publicDealOfferAction(['b'], 'Terms', {...payment,payer:'b',recipient:'a'}, 'a', 2, travel), null);
@@ -75,7 +81,7 @@ void test('the combined offer explains payment timing and the recipient can acce
});
/** Stateful VM exercises the actual authoring callbacks without claiming browser hydration. */
function combinedForm() {
function combinedForm(gameOverride?: GameView) {
const states:unknown[] = [], sent:Action[] = [];
let cursor = 0;
const exported:{PublicDeals?:typeof PublicDeals} = {};
@@ -96,7 +102,7 @@ function combinedForm() {
throw new Error(`Unexpected import ${name}`);
}});
assert.ok(exported.PublicDeals);
const game = {...base, me:'a', deals:[]} as GameView;
const game = gameOverride ?? {...base, me:'a', deals:[]} as GameView;
const render = () => {cursor = 0; return elements(exported.PublicDeals!({game,busy:false,act(action) {sent.push(action);}}));};
const check = (index:number) => {
const node = render().filter(element => element.type === 'input' && element.props.type === 'checkbox')[index];
@@ -156,6 +162,62 @@ void test('the public deal shows the exact recipient, travel intent, turn and au
assert.match(markup({...base, dealTravelStatus:{deal:status}} as unknown as GameView), new RegExp(`Travel status: ${status}`));
});
void test('group offers name the traveler, require their last vote and still permit declining early', () => {
const players = [...base.players, {id:'c',name:'Cora',faction:'emperor',ally:null},
{id:'d',name:'Dara',faction:'guild',ally:null}];
const group = {...deal, participants:['a','b','c','d'], acceptVotes:['a','b'], status:'offered',
travel:{...travel,actor:'c'}};
const game = {...base, players, deals:[group], me:'c', dealTravelStatus:{}} as unknown as GameView;
assert.match(markup(game), /Cora \(Emperor\) would make a reserve shipment/);
assert.match(markup(game), /Cora \(Emperor\) must accept last/);
const actions:Action[] = [];
const early = elements(PublicDealList({game,disabled:false,act(action) {actions.push(action);}}));
assert.equal(early.find(node => node.props.children === 'Accept deal')?.props.disabled,true);
const decline = early.find(node => node.props.children === 'Decline deal');
assert.ok(decline); assert.equal(decline.props.disabled,false);
(decline.props.onClick as () => void)();
assert.deepEqual(actions,[{type:'dealRespond',deal:'deal',accept:false}]);
assert.deepEqual(publicDealRequests(game), [['offer','deal','waiting']], 'The traveler can see and decline the offer before other votes are in.');
const ordinary = {...game,me:'d'};
assert.deepEqual(publicDealRequests(ordinary), [['offer','deal']]);
assert.equal(elements(PublicDealList({game:ordinary,disabled:false,act() {}}))
.find(node => node.props.children === 'Accept deal')?.props.disabled,false);
const final = {...game,deals:[{...group,acceptVotes:['a','b','d']}]} as unknown as GameView;
assert.deepEqual(publicDealRequests(final), [['offer','deal','ready']], 'A changed request reopens the panel for the final vote.');
const finalActions:Action[] = [];
const accept = elements(PublicDealList({game:final,disabled:false,act(action) {finalActions.push(action);}}))
.find(node => node.props.children === 'Accept deal');
assert.ok(accept); assert.equal(accept.props.disabled,false);
(accept.props.onClick as () => void)();
assert.deepEqual(finalActions.at(-1),{type:'dealRespond',deal:'deal',accept:true});
const active = {...game, deals:[{...group,status:'active',acceptVotes:['a','b','c','d']}],
dealTravelStatus:{deal:'pending'}} as unknown as GameView;
assert.deepEqual(publicDealRequests(active), [['travel','deal']]);
assert.deepEqual(publicDealRequests({...active,me:'b'}), []);
});
void test('the group offer form selects an invited traveler and revalidates participant changes', () => {
const game = {...base, me:'a', deals:[], players:[...base.players,
{id:'c',name:'Cora',faction:'emperor',ally:null}, {id:'d',name:'Dara',faction:'guild',ally:null}]} as unknown as GameView;
const ui = combinedForm(game);
ui.check(0); ui.check(1); ui.check(4); // Basil, Cora, then travel.
const button = () => ui.render().find(node => node.type === 'button' && node.props.type === 'submit');
ui.select(0,'c'); ui.select(3,'carthag');
const terms = ui.render().find(node => node.type === 'textarea'); assert.ok(terms);
(terms.props.onChange as (event:{target:{value:string}}) => void)({target:{value:'Group travel'}});
assert.equal(button()?.props.disabled,false);
const form = ui.render().find(node => node.type === 'form'); assert.ok(form);
(form.props.onSubmit as (event:{preventDefault():void}) => void)({preventDefault() {}});
assert.deepEqual(JSON.parse(JSON.stringify(ui.sent[0])), {type:'dealOffer',targets:['b','c'],terms:'Group travel',
travel:{kind:'ship',territory:'carthag',minimum:1,answer:true,actor:'c'}});
const cora = ui.render().filter(node => node.type === 'input' && node.props.type === 'checkbox')[1];
(cora.props.onChange as (event:{target:{checked:boolean}}) => void)({target:{checked:false}});
ui.check(2); // Add Dara after removing the chosen traveler.
assert.equal(button()?.props.disabled,true);
ui.select(0,'d'); assert.equal(button()?.props.disabled,false);
ui.check(3); assert.equal(button()?.props.disabled,true, 'Group travel cannot be combined with payment.');
});
void test('only the obligated recipient gets a persistent pending reminder', () => {
assert.deepEqual(publicDealRequests(base), [['travel','deal']]);
assert.match(renderToStaticMarkup(createElement(PublicDeals,{game:base,act() {},busy:false})), /Public deals · travel term pending/);
@@ -211,9 +273,9 @@ void test('the recipient guide distinguishes a void deal from unanimous release'
}
});
void test('the real offer form explains single-recipient eligibility before a travel term is selected', () => {
void test('the real offer form asks for recipients before a travel term is selected', () => {
const html = renderToStaticMarkup(createElement(PublicDeals,{game:base,act() {},busy:false}));
assert.match(html, /Include a current-turn travel term/);
assert.match(html, /Select exactly one human recipient to add a travel term/);
assert.match(html, /Select at least one human recipient to add a travel term/);
assert.match(html, /cannot automatically enforce free-text promises/);
});
+57 -2
View File
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import {
createMultipartyPublicDeal, createPublicDeal, parsePublicDealTravel, PublicDealError,
publicDealTravelActor,
releasePublicDeal, respondPublicDeal, setPublicDealTravel, validatePublicDeals, withdrawPublicDeal,
type PublicDeal, type PublicDealSeat,
} from '../game/public-deals';
@@ -22,8 +23,10 @@ void test('travel terms accept only exact bounded native actions and an explicit
{ ...term, kind: 'fly' }, { ...term, territory: 'not_a_territory' },
...[0, -1, 21, 1.5, Infinity, Number.MAX_SAFE_INTEGER + 1, '1'].map(minimum => ({ ...term, minimum })),
{ ...term, answer: 'yes' }, { ...term, answer: undefined },
{ ...term, sector: 0 }, { ...term, turn: 4 }, { ...term, actor: 'a' },
{ ...term, sector: 0 }, { ...term, turn: 4 }, { ...term, actor: '' }, { ...term, actor: undefined },
{ ...term, actor: ' c ' },
]) assert.throws(() => parsePublicDealTravel(invalid), PublicDealError);
assert.deepEqual(parsePublicDealTravel({...term, actor:'c'}), {...term, actor:'c'});
});
void test('a recipient accepts and both release an unchanged current-turn travel term', () => {
@@ -34,6 +37,7 @@ void test('a recipient accepts and both release an unchanged current-turn travel
assert.equal(source.travel, undefined);
assert.deepEqual(offered.travel, term);
assert.equal(offered.turn, 3);
assert.equal(publicDealTravelActor(offered), 'b');
assert.throws(() => respondPublicDeal(offered, seats, 'a', true), PublicDealError);
assert.throws(() => respondPublicDeal(offered, seats, 'c', true), PublicDealError);
const active = respondPublicDeal(offered, seats, 'b', true);
@@ -57,7 +61,7 @@ void test('a recipient accepts and both release an unchanged current-turn travel
assert.deepEqual(withdrawPublicDeal(offered, seats, 'a').travel, term);
});
void test('travel is incompatible with multi-party, replacement and malformed persisted terms', () => {
void test('travel requires an actor for multiparty offers and cannot replace an existing term', () => {
const multi = createMultipartyPublicDeal(seats, 'a', ['b', 'c'], 'Terms', 3, 'multi');
const offered = setPublicDealTravel(fresh(), term, seats);
const active = respondPublicDeal(offered, seats, 'b', true);
@@ -65,11 +69,62 @@ void test('travel is incompatible with multi-party, replacement and malformed pe
assert.throws(() => setPublicDealTravel(deal, term, seats), PublicDealError);
for (const invalid of [
{ ...multi, travel: term },
{ ...fresh(), travel: {...term, actor:'b'} },
{ ...fresh(), travel: undefined }, { ...fresh(), travel: { ...term, sector: 0 } },
{ ...offered, paymentStatus: 'pending' }, { ...offered, travelStatus: 'pending' },
]) assert.throws(() => validatePublicDeals([invalid], seats), PublicDealError);
});
void test('multiparty travel requires the committed recipient to accept last and all parties to release', () => {
const people: PublicDealSeat[] = ['a','b','c','d','e','f'].map(id => ({id}));
for (const actor of ['b','c','d','e','f']) {
const multi = createMultipartyPublicDeal(people, 'a', ['b','c','d','e','f'], 'Travel together.', 3, `multi-${actor}`);
const offered = setPublicDealTravel(multi, {...term,actor}, people);
assert.deepEqual(offered.travel, {...term,actor});
assert.equal(publicDealTravelActor(offered), actor);
assert.equal(multi.travel, undefined);
assert.throws(() => respondPublicDeal(offered, people, actor, true), PublicDealError);
assert.equal(respondPublicDeal(offered, people, actor, false).status, 'declined');
assert.equal(withdrawPublicDeal(offered, people, 'a').status, 'withdrawn');
let current = offered;
// Deliberately vote in reverse roster order; serialization stays canonical.
for (const voter of ['f','e','d','c','b'].filter(id => id !== actor)) {
current = respondPublicDeal(current, people, voter, true);
assert.equal(current.status, 'offered');
if (current.acceptVotes!.length < people.length - 1)
assert.throws(() => respondPublicDeal(current, people, actor, true), PublicDealError);
}
const last = respondPublicDeal(current, people, actor, true);
assert.equal(last.status, 'active');
assert.deepEqual(last.acceptVotes, ['a','b','c','d','e','f']);
let released = last;
for (const voter of ['f','e','d','c','b','a']) released = releasePublicDeal(released, people, voter, true);
assert.equal(released.status, 'released');
for (const snapshot of [offered,current,last,released]) {
const copy = JSON.parse(JSON.stringify(snapshot)) as PublicDeal;
validatePublicDeals([copy], people);
assert.deepEqual(copy.travel, {...term,actor});
}
}
});
void test('multiparty travel rejects forged actors, early votes, payments and nonfresh offers', () => {
const multi = createMultipartyPublicDeal(seats, 'a', ['b','c'], 'Terms', 3, 'multi');
for (const actor of ['a','d','',undefined]) {
const invalid = {...term, actor};
assert.throws(() => setPublicDealTravel(multi, invalid, seats), PublicDealError);
assert.throws(() => validatePublicDeals([{...multi, travel:invalid}], seats), PublicDealError);
}
const offered = setPublicDealTravel(multi, {...term,actor:'b'}, seats);
for (const status of ['offered','withdrawn','declined'] as const) {
assert.throws(() => validatePublicDeals([{...offered,status,acceptVotes:['a','b']}], seats), PublicDealError);
}
const acceptedOther = respondPublicDeal(offered, seats, 'c', true);
assert.throws(() => setPublicDealTravel(respondPublicDeal(multi, seats, 'c', true), {...term,actor:'b'}, seats), PublicDealError);
assert.throws(() => validatePublicDeals([{...offered,payment:{kind:'immediateBribe',payer:'a',recipient:'b',amount:1}}], seats), PublicDealError);
assert.equal(respondPublicDeal(acceptedOther, seats, 'b', true).status, 'active');
});
void test('a proposer-to-recipient immediate payment can accompany travel through the complete deal lifecycle', () => {
const payment = { kind: 'immediateBribe', payer: 'a', recipient: 'b', amount: 3 } as const;
const original = createPublicDeal(seats, 'a', 'b', 'Payment for travel.', 3, 'paid-travel', payment);