Support paid current-turn travel deals
Verify and publish checkpoint container / container (push) Successful in 10m34s
Verify and publish checkpoint container / container (push) Successful in 10m34s
This commit is contained in:
1 parent
82c639c977
commit
a1b2aacbac
9 files changed
+371
-23
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { actTruthAlliance as act, advanceTruthAlliance as advance, createTruthAllianceFixture,
|
||||
truthAllianceClean as clean, truthAllianceInventory as inventory, truthAlliancePlayer as player } from './fixture-truth-alliance';
|
||||
import { declareTruthBribe } from './fixture-truth-bribe';
|
||||
|
||||
/** The original Basic H/A/E setup, real worm/Nexus alliance and later Bidding
|
||||
* from the shared fixture. The offer, Truthtrance question, answer and final
|
||||
* consent are all real actions; no wallet, pledge or commitment is staged. */
|
||||
void test('a previously offered paid travel deal can release a later unaffordable pledge on recipient consent', () => {
|
||||
const f = createTruthAllianceFixture({ consent: 'allied' });
|
||||
let game = advance(f.game, state => state.phase === 3 && !!state.auction && clean(state));
|
||||
assert.equal(player(game, f.target).ally, f.partner);
|
||||
assert.equal(player(game, f.partner).ally, f.target);
|
||||
assert.equal(player(game, f.asker).ally, null);
|
||||
const minimum = 4, initialCash = player(game, f.target).spice;
|
||||
const amount = initialCash - minimum + 1;
|
||||
assert.ok(amount > 0);
|
||||
game = act(game, f.target, { type: 'dealOffer', target: f.asker,
|
||||
terms: 'Immediate payment for a current-turn travel restriction.',
|
||||
payment: { kind: 'immediateBribe', payer: f.target, recipient: f.asker, amount },
|
||||
travel: { kind: 'ship', territory: 'polar_sink', minimum: 1, answer: false } });
|
||||
const offer = game.deals?.at(-1); assert.ok(offer);
|
||||
game = declareTruthBribe(f, game).game;
|
||||
assert.ok(viewGame(game, f.asker).truthPledgeTargets.includes(f.target));
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'pledgeAid', target: f.target,
|
||||
turn: game.turn, phase: game.phase, ally: f.partner, minimum } });
|
||||
assert.ok(viewGame(game, f.target).truthPledgeAnswers?.includes('yes'));
|
||||
game = act(game, f.target, { type: 'truthAnswer', answer: 'yes' });
|
||||
const pledge = game.pledgePromises?.at(-1); assert.ok(pledge);
|
||||
assert.ok(!pledge.fulfilled && !pledge.released);
|
||||
|
||||
// A new voluntary payment cannot evade the later pledge, but the original
|
||||
// offer already consented to payment before that answer was made.
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => act(game, f.target, { type: 'bribe', target: f.asker, amount }));
|
||||
assert.deepEqual(game, before);
|
||||
const incoming = player(game, f.asker).bribes;
|
||||
const next = act(game, f.asker, { type: 'dealRespond', deal: offer.id, accept: true });
|
||||
assert.equal(next.deals?.find(deal => deal.id === offer.id)?.status, 'active');
|
||||
assert.equal(player(next, f.target).spice, initialCash - amount);
|
||||
assert.equal(player(next, f.asker).bribes, incoming + amount);
|
||||
assert.deepEqual(next.aid[f.target], before.aid[f.target]);
|
||||
assert.equal(next.pledgePromises?.at(-1)?.released, true);
|
||||
assert.ok(!next.pledgePromises?.at(-1)?.fulfilled);
|
||||
assert.equal(next.travelPromises?.filter(row => row.sourceDeal === offer.id).length, 1);
|
||||
assert.equal(viewGame(next, f.asker).dealTravelStatus[offer.id], 'pending');
|
||||
assert.deepEqual(inventory(next), f.originalCards);
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import { actTruthTravel as act, advanceTruthTravel as advance, createTruthTravelFixture as fixture,
|
||||
settleTruthTravel as settle, truthTravelClean as clean, truthTravelInventory as inventory,
|
||||
truthTravelPlayer as player } from './fixture-truth-travel';
|
||||
|
||||
type Term = { kind: 'ship' | 'move'; territory: string; minimum: number; answer: boolean };
|
||||
function reject(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => act(game, actor, action));
|
||||
assert.deepEqual(game, before, 'Failed consent and action packets preserve the source table.');
|
||||
}
|
||||
function offer(game: Game, proposer: string, target: string, travel: Term, amount: number) {
|
||||
const payment = { kind: 'immediateBribe', payer: proposer, recipient: target, amount };
|
||||
const next = act(game, proposer, { type: 'dealOffer', target, terms: 'Payment for agreed travel.', payment, travel });
|
||||
const deal = next.deals?.at(-1); assert.ok(deal);
|
||||
return { game: next, id: deal.id, payment, travel };
|
||||
}
|
||||
function accept(game: Game, target: string, id: string) {
|
||||
return act(game, target, { type: 'dealRespond', deal: id, accept: true });
|
||||
}
|
||||
function status(game: Game, viewer: string, id: string) {
|
||||
return viewGame(game, viewer).dealTravelStatus[id];
|
||||
}
|
||||
|
||||
/** Native Basic Atreides/Emperor game at the real pre-shipment or shipped
|
||||
* phase-five boundary. Only original unopened deals/Spice and scalar lotteries
|
||||
* are selected by the shared TruthTravel fixture; all agreements and payments
|
||||
* below are real game actions. */
|
||||
void test('final consent atomically pays the proposer bribe and creates the travel obligation', () => {
|
||||
const f = fixture(), initial = f.game;
|
||||
const beforePayer = player(initial, f.asker).spice, beforeCash = player(initial, f.target).spice,
|
||||
beforeEscrow = player(initial, f.target).bribes;
|
||||
assert.ok(beforePayer >= 3);
|
||||
const offered = offer(initial, f.asker, f.target,
|
||||
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, 3);
|
||||
assert.equal(player(offered.game, f.asker).spice, beforePayer);
|
||||
assert.equal(player(offered.game, f.target).bribes, beforeEscrow);
|
||||
const game = accept(offered.game, f.target, offered.id);
|
||||
assert.equal(game.deals!.find(deal => deal.id === offered.id)!.status, 'active');
|
||||
assert.equal(player(game, f.asker).spice, beforePayer - 3);
|
||||
assert.equal(player(game, f.target).spice, beforeCash);
|
||||
assert.equal(player(game, f.target).bribes, beforeEscrow + 3);
|
||||
assert.equal(game.travelPromises!.filter(row => row.sourceDeal === offered.id).length, 1);
|
||||
assert.equal(status(game, f.target, offered.id), 'pending');
|
||||
reject(game, f.target, { type: 'dealRespond', deal: offered.id, accept: true });
|
||||
const next = settle(act(game, f.target, f.shipmentAction));
|
||||
assert.equal(status(next, f.target, offered.id), 'fulfilled');
|
||||
assert.equal(player(next, f.target).spice, beforeCash - f.shipmentCost);
|
||||
assert.equal(player(next, f.target).bribes, beforeEscrow + 3);
|
||||
assert.deepEqual(inventory(next), f.originalCards);
|
||||
|
||||
const collection = advance(next, state => state.turn === initial.turn && state.phase === 7 && clean(state));
|
||||
const cashBeforeMentat = player(collection, f.target).spice;
|
||||
assert.equal(player(collection, f.target).bribes, beforeEscrow + 3);
|
||||
const mentat = advance(collection, state => state.turn === initial.turn && state.phase === 8 && clean(state));
|
||||
assert.equal(player(mentat, f.target).spice, cashBeforeMentat + beforeEscrow + 3);
|
||||
assert.equal(player(mentat, f.target).bribes, 0);
|
||||
assert.deepEqual(inventory(mentat), f.originalCards);
|
||||
});
|
||||
|
||||
void test('a payer who spends after offering cannot partly activate or pay the deal', () => {
|
||||
const f = fixture(), payerCash = player(f.game, f.asker).spice; assert.ok(payerCash >= 1);
|
||||
const offered = offer(f.game, f.asker, f.target,
|
||||
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, payerCash);
|
||||
const spent = act(offered.game, f.asker, { type: 'bribe', target: f.target, amount: 1 });
|
||||
assert.equal(player(spent, f.asker).spice, payerCash - 1);
|
||||
const before = structuredClone(spent);
|
||||
reject(spent, f.target, { type: 'dealRespond', deal: offered.id, accept: true });
|
||||
assert.deepEqual(spent, before);
|
||||
assert.equal(spent.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
|
||||
assert.equal(spent.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
|
||||
assert.deepEqual(inventory(spent), f.originalCards);
|
||||
});
|
||||
|
||||
void test('an infeasible promised shipment rolls back payment and final acceptance', () => {
|
||||
const f = fixture({ window: 'shipped' }), before = f.game;
|
||||
const offered = offer(before, f.asker, f.target,
|
||||
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, 1);
|
||||
const payer = player(offered.game, f.asker).spice, cash = player(offered.game, f.target).spice,
|
||||
escrow = player(offered.game, f.target).bribes;
|
||||
reject(offered.game, f.target, { type: 'dealRespond', deal: offered.id, accept: true });
|
||||
assert.equal(offered.game.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
|
||||
assert.equal(player(offered.game, f.asker).spice, payer);
|
||||
assert.equal(player(offered.game, f.target).spice, cash);
|
||||
assert.equal(player(offered.game, f.target).bribes, escrow);
|
||||
assert.equal(offered.game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
|
||||
});
|
||||
|
||||
void test('the recipient cannot use incoming Mentat escrow to fund the promised shipment', () => {
|
||||
const f = fixture(), cash = player(f.game, f.target).spice; assert.ok(cash >= 2);
|
||||
const spent = act(f.game, f.target, { type: 'bribe', target: f.asker, amount: cash - 1 });
|
||||
assert.equal(player(spent, f.target).spice, 1);
|
||||
assert.ok(player(spent, f.asker).spice >= 3);
|
||||
const offered = offer(spent, f.asker, f.target,
|
||||
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, 3);
|
||||
reject(offered.game, f.target, { type: 'dealRespond', deal: offered.id, accept: true });
|
||||
assert.equal(player(offered.game, f.target).spice, 1);
|
||||
assert.equal(offered.game.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
|
||||
assert.equal(offered.game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
|
||||
});
|
||||
|
||||
void test('unanimous release ends the pending travel obligation without refunding payment', () => {
|
||||
const f = fixture(), offered = offer(f.game, f.asker, f.target,
|
||||
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, 1);
|
||||
let game = accept(offered.game, f.target, offered.id);
|
||||
const paid = player(game, f.asker).spice, escrow = player(game, f.target).bribes;
|
||||
game = act(game, f.asker, { type: 'dealRelease', deal: offered.id, agree: true });
|
||||
assert.equal(status(game, f.target, offered.id), 'pending');
|
||||
game = act(game, f.target, { type: 'dealRelease', deal: offered.id, agree: true });
|
||||
assert.equal(status(game, f.target, offered.id), 'released');
|
||||
assert.equal(player(game, f.asker).spice, paid);
|
||||
assert.equal(player(game, f.target).bribes, escrow);
|
||||
game = settle(act(game, f.target, f.currentEnd));
|
||||
assert.equal(player(game, f.asker).spice, paid);
|
||||
assert.equal(player(game, f.target).bribes, escrow);
|
||||
});
|
||||
|
||||
void test('paid travel excludes reversed, deferred and multiparty payment variants', () => {
|
||||
const f = fixture(), travel = { kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true };
|
||||
const base = { type: 'dealOffer', target: f.target, terms: 'Travel.', travel } satisfies Action;
|
||||
reject(f.game, f.asker, { ...base, payment: { kind: 'immediateBribe', payer: f.target, recipient: f.asker, amount: 1 } });
|
||||
reject(f.game, f.asker, { ...base, payment: { kind: 'deferredBribe', payer: f.asker, recipient: f.target, amount: 1, dueTurn: f.game.turn } });
|
||||
reject(f.game, f.asker, { type: 'dealOffer', targets: [f.target, f.asker], terms: 'Travel.', travel,
|
||||
payment: { kind: 'immediateBribe', payer: f.asker, recipient: f.target, amount: 1 } });
|
||||
});
|
||||
@@ -1,7 +1,15 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { runInNewContext } from 'node:vm';
|
||||
import { createElement, isValidElement, type ReactElement, type ReactNode } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import * as jsxRuntime from 'react/jsx-runtime';
|
||||
import ts from 'typescript';
|
||||
import * as board from '../game/board';
|
||||
import * as catalog from '../game/catalog';
|
||||
import * as coalitionAlliance from '../game/coalition-alliance';
|
||||
import * as publicDeals from '../game/public-deals';
|
||||
import { PublicDealList, PublicDeals, publicDealOfferAction, publicDealRequests } from '../components/public-deals';
|
||||
import { TravelPromises } from '../components/travel-promises';
|
||||
import type { Action, GameView } from '../game/engine';
|
||||
@@ -29,7 +37,11 @@ void test('the offer packet retains the original recipient action and validates
|
||||
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.equal(publicDealOfferAction(['b'], 'Terms', payment, 'a', 2, travel), 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);
|
||||
assert.equal(publicDealOfferAction(['b'], 'Terms', {kind:'deferredBribe',payer:'a',recipient:'b',amount:2,dueTurn:2}, 'a', 2, travel), null);
|
||||
assert.equal(publicDealOfferAction(['b','c'], 'Terms', payment, 'a', 2, travel), null);
|
||||
assert.equal(publicDealOfferAction(['a'], 'Terms', undefined, 'a', 2, travel), null);
|
||||
assert.equal(publicDealOfferAction(['b'], 'Terms', undefined, undefined, 2, travel), null);
|
||||
for (const invalid of [
|
||||
@@ -43,6 +55,94 @@ void test('the offer packet retains the original recipient action and validates
|
||||
assert.deepEqual(publicDealOfferAction(['b','c'], 'Terms'), {type:'dealOffer', targets:['b','c'], terms:'Terms'});
|
||||
});
|
||||
|
||||
void test('the combined offer explains payment timing and the recipient can accept or decline', () => {
|
||||
const offered = {...base, deals:[{...deal,status:'offered',payment}], dealTravelStatus:{},
|
||||
bribeOptions:{blocked:null}} as unknown as GameView;
|
||||
const html = markup(offered);
|
||||
assert.match(html, /Alice \(Atreides\) pays 2 spice to Basil \(Harkonnen\) on final acceptance/);
|
||||
assert.match(html, /recipient collects their share at the next Mentat opening/);
|
||||
assert.match(html, /cannot use this payment to fund the current travel/);
|
||||
const sent:Action[] = [];
|
||||
const tree = PublicDealList({game:offered, disabled:false, act(action) {sent.push(action);}});
|
||||
const accept = elements(tree).find(node => node.props.children === 'Accept deal');
|
||||
const decline = elements(tree).find(node => node.props.children === 'Decline deal');
|
||||
assert.ok(accept); assert.ok(decline);
|
||||
assert.equal(accept.props.disabled, false); assert.equal(decline.props.disabled, false);
|
||||
(accept.props.onClick as () => void)();
|
||||
assert.deepEqual(sent, [{type:'dealRespond',deal:'deal',accept:true}]);
|
||||
const busyTree = PublicDealList({game:offered, disabled:true, act() {}});
|
||||
assert.equal(elements(busyTree).find(node => node.props.children === 'Accept deal')?.props.disabled,true);
|
||||
});
|
||||
|
||||
/** Stateful VM exercises the actual authoring callbacks without claiming browser hydration. */
|
||||
function combinedForm() {
|
||||
const states:unknown[] = [], sent:Action[] = [];
|
||||
let cursor = 0;
|
||||
const exported:{PublicDeals?:typeof PublicDeals} = {};
|
||||
runInNewContext(ts.transpileModule(readFileSync(new URL('../components/public-deals.tsx', import.meta.url),'utf8'), {
|
||||
compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX},
|
||||
}).outputText, {exports:exported, require(name:string) {
|
||||
if (name === 'react/jsx-runtime') return jsxRuntime;
|
||||
if (name === 'react') return {useState(initial:unknown) {
|
||||
const index = cursor++;
|
||||
if (!(index in states)) states[index] = typeof initial === 'function' ? initial() : initial;
|
||||
return [states[index], (next:unknown) => {states[index] = typeof next === 'function' ? next(states[index]) : next;}];
|
||||
}, useRef:() => ({current:null}), useEffect:() => {}};
|
||||
if (name === '@/components/ui/button') return {Button:'button'};
|
||||
if (name === '@/game/board') return board;
|
||||
if (name === '@/game/catalog') return catalog;
|
||||
if (name === '@/game/coalition-alliance') return coalitionAlliance;
|
||||
if (name === '@/game/public-deals') return publicDeals;
|
||||
throw new Error(`Unexpected import ${name}`);
|
||||
}});
|
||||
assert.ok(exported.PublicDeals);
|
||||
const game = {...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];
|
||||
assert.ok(node); assert.equal(node.props.disabled,false);
|
||||
(node.props.onChange as (event:{target:{checked:boolean}}) => void)({target:{checked:true}});
|
||||
};
|
||||
const select = (index:number,value:string) => {
|
||||
const node = render().filter(element => element.type === 'select')[index]; assert.ok(node);
|
||||
(node.props.onChange as (event:{target:{value:string}}) => void)({target:{value}});
|
||||
};
|
||||
return {render, check, select, sent};
|
||||
}
|
||||
|
||||
void test('the authoring controls compose a proposer payment with recipient travel and reject invalid combinations', () => {
|
||||
const ui = combinedForm();
|
||||
ui.check(0); // Select recipient.
|
||||
ui.check(1); // Include payment.
|
||||
ui.select(1,'a'); ui.select(2,'b');
|
||||
ui.check(2); // Include travel while payment is selected.
|
||||
ui.select(5,'carthag');
|
||||
const terms = ui.render().find(node => node.type === 'textarea'); assert.ok(terms);
|
||||
(terms.props.onChange as (event:{target:{value:string}}) => void)({target:{value:'Travel for spice'}});
|
||||
const button = () => ui.render().find(node => node.type === 'button' && node.props.type === 'submit');
|
||||
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',target:'b',terms:'Travel for spice',
|
||||
payment:{kind:'immediateBribe',payer:'a',recipient:'b',amount:1},
|
||||
travel:{kind:'ship',territory:'carthag',minimum:1,answer:true}});
|
||||
ui.select(0,'deferredBribe'); assert.equal(button()?.props.disabled,true);
|
||||
ui.select(0,'immediateBribe'); ui.select(1,'b'); ui.select(2,'a');
|
||||
assert.equal(button()?.props.disabled,true);
|
||||
});
|
||||
|
||||
void test('a payment can also be added after selecting the travel term', () => {
|
||||
const ui = combinedForm();
|
||||
ui.check(0); ui.check(2);
|
||||
ui.select(2,'carthag');
|
||||
ui.check(1);
|
||||
ui.select(1,'a'); ui.select(2,'b');
|
||||
const terms = ui.render().find(node => node.type === 'textarea'); assert.ok(terms);
|
||||
(terms.props.onChange as (event:{target:{value:string}}) => void)({target:{value:'Travel for spice'}});
|
||||
const submit = ui.render().find(node => node.type === 'button' && node.props.type === 'submit');
|
||||
assert.equal(submit?.props.disabled,false);
|
||||
});
|
||||
|
||||
void test('the public deal shows the exact recipient, travel intent, turn and authoritative status', () => {
|
||||
const html = markup(base);
|
||||
assert.match(html, /Current-turn travel term/);
|
||||
|
||||
@@ -57,21 +57,61 @@ 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, payment, replacement and malformed persisted terms', () => {
|
||||
void test('travel is incompatible with multi-party, replacement and malformed persisted terms', () => {
|
||||
const multi = createMultipartyPublicDeal(seats, 'a', ['b', 'c'], 'Terms', 3, 'multi');
|
||||
const payment = createPublicDeal(seats, 'a', 'b', 'Terms', 3, 'paid',
|
||||
{ kind: 'immediateBribe', payer: 'a', recipient: 'b', amount: 1 });
|
||||
const offered = setPublicDealTravel(fresh(), term, seats);
|
||||
const active = respondPublicDeal(offered, seats, 'b', true);
|
||||
for (const deal of [multi, payment, offered, active])
|
||||
for (const deal of [multi, offered, active])
|
||||
assert.throws(() => setPublicDealTravel(deal, term, seats), PublicDealError);
|
||||
for (const invalid of [
|
||||
{ ...multi, travel: term }, { ...payment, travel: term },
|
||||
{ ...multi, travel: term },
|
||||
{ ...fresh(), travel: undefined }, { ...fresh(), travel: { ...term, sector: 0 } },
|
||||
{ ...offered, paymentStatus: 'pending' }, { ...offered, travelStatus: 'pending' },
|
||||
]) assert.throws(() => validatePublicDeals([invalid], seats), PublicDealError);
|
||||
});
|
||||
|
||||
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);
|
||||
const offered = setPublicDealTravel(original, term, seats);
|
||||
assert.equal(original.travel, undefined);
|
||||
const active = respondPublicDeal(offered, seats, 'b', true);
|
||||
const released = releasePublicDeal(releasePublicDeal(active, seats, 'a', true), seats, 'b', true);
|
||||
for (const deal of [offered, active, released, respondPublicDeal(offered, seats, 'b', false),
|
||||
withdrawPublicDeal(offered, seats, 'a')]) {
|
||||
const roundtrip = JSON.parse(JSON.stringify(deal)) as PublicDeal;
|
||||
const before = JSON.stringify(roundtrip);
|
||||
validatePublicDeals([roundtrip], seats);
|
||||
assert.equal(JSON.stringify(roundtrip), before);
|
||||
assert.deepEqual(roundtrip.payment, payment);
|
||||
assert.deepEqual(roundtrip.travel, term);
|
||||
assert.equal(Object.hasOwn(roundtrip, 'paymentStatus'), false);
|
||||
}
|
||||
assert.equal(active.status, 'active');
|
||||
assert.equal(released.status, 'released');
|
||||
});
|
||||
|
||||
void test('paid travel rejects reverse, deferred, multiparty and forged persisted payment shapes', () => {
|
||||
const reverse = { kind: 'immediateBribe', payer: 'b', recipient: 'a', amount: 1 } as const;
|
||||
const deferred = { kind: 'deferredBribe', payer: 'a', recipient: 'b', amount: 1, dueTurn: 3 } as const;
|
||||
const paid = { kind: 'immediateBribe', payer: 'a', recipient: 'b', amount: 1 } as const;
|
||||
for (const payment of [reverse, deferred]) {
|
||||
const offer = createPublicDeal(seats, 'a', 'b', 'Terms', 3, `invalid-${payment.kind}`, payment);
|
||||
assert.throws(() => setPublicDealTravel(offer, term, seats), PublicDealError);
|
||||
assert.throws(() => validatePublicDeals([{ ...offer, travel: term }], seats), PublicDealError);
|
||||
}
|
||||
const multi = createMultipartyPublicDeal(seats, 'a', ['b', 'c'], 'Terms', 3, 'multi-paid', paid);
|
||||
assert.throws(() => setPublicDealTravel(multi, term, seats), PublicDealError);
|
||||
for (const invalid of [
|
||||
{ ...multi, travel: term },
|
||||
{ ...fresh(), payment: { ...paid, recipient: 'c' }, travel: term },
|
||||
{ ...fresh(), payment: { ...paid, amount: 0 }, travel: term },
|
||||
{ ...fresh(), payment: { ...paid, extra: true }, travel: term },
|
||||
{ ...fresh(), payment: undefined, travel: term },
|
||||
{ ...fresh(), payment: paid, paymentStatus: 'paid', travel: term },
|
||||
]) assert.throws(() => validatePublicDeals([invalid], seats), PublicDealError);
|
||||
});
|
||||
|
||||
void test('legacy two-party and multi-party records validate without acquiring travel fields', () => {
|
||||
const legacy = fresh();
|
||||
const multi = createMultipartyPublicDeal(seats, 'a', ['b', 'c'], 'Terms', 3, 'multi');
|
||||
|
||||
Reference in new issue
Block a user