99 lines
5.8 KiB
TypeScript
99 lines
5.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import {
|
||
createMultipartyPublicDeal, createPublicDeal, markPublicDealPaid, parsePublicDealPayment,
|
||
PublicDealError, releasePublicDeal, respondPublicDeal, validatePublicDeals,
|
||
voidPublicDealPayment, type PublicDeal, type PublicDealSeat,
|
||
} from '../game/public-deals';
|
||
import {
|
||
pendingPublicDealPayments, publicDealPaymentTotal, PublicDealObligationError,
|
||
reconcilePublicDealPayments,
|
||
} from '../game/public-deal-obligations';
|
||
|
||
const seats: PublicDealSeat[] = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
|
||
const deferred = (amount: number) => ({
|
||
kind: 'deferredBribe' as const, payer: 'a', recipient: 'b', amount, dueTurn: 3,
|
||
});
|
||
const accept = (id: string, amount: number) =>
|
||
respondPublicDeal(createPublicDeal(seats, 'a', 'b', 'Pay during this turn', 3, id, deferred(amount)), seats, 'b', true);
|
||
|
||
void test('deferred deal terms bind an exact positive payment to this turn without changing immediate terms', () => {
|
||
const immediate = { kind: 'immediateBribe', payer: 'a', recipient: 'b', amount: 2 } as const;
|
||
assert.deepEqual(parsePublicDealPayment(immediate, ['a', 'b']), immediate);
|
||
assert.deepEqual(parsePublicDealPayment(deferred(4), ['a', 'b'], 3), deferred(4));
|
||
for (const input of [
|
||
{ ...deferred(4), dueTurn: 4 }, { ...deferred(0) },
|
||
{ ...deferred(4), recipient: 'c' }, { ...deferred(4), extra: true },
|
||
]) assert.throws(() => parsePublicDealPayment(input, ['a', 'b'], 3), PublicDealError);
|
||
assert.throws(() => parsePublicDealPayment(deferred(4), ['a', 'b']), PublicDealError);
|
||
assert.throws(() => createPublicDeal(seats, 'a', 'b', 'Pay later', 4, 'wrong-turn', deferred(4)), PublicDealError);
|
||
});
|
||
|
||
void test('all signatories accept before a future payment becomes pending; only its payer can settle it once', () => {
|
||
const offered = createMultipartyPublicDeal(seats, 'a', ['b', 'c'], 'Pay later', 3, 'group', deferred(4));
|
||
const partially = respondPublicDeal(offered, seats, 'b', true);
|
||
assert.equal(partially.status, 'offered');
|
||
assert.equal(partially.paymentStatus, undefined);
|
||
const active = respondPublicDeal(partially, seats, 'c', true);
|
||
assert.equal(active.paymentStatus, 'pending');
|
||
assert.throws(() => markPublicDealPaid(active, seats, 'b'), PublicDealError);
|
||
const paid = markPublicDealPaid(active, seats, 'a');
|
||
assert.equal(paid.paymentStatus, 'paid');
|
||
assert.throws(() => markPublicDealPaid(paid, seats, 'a'), PublicDealError);
|
||
let released = releasePublicDeal(paid, seats, 'a', true);
|
||
released = releasePublicDeal(released, seats, 'b', true);
|
||
released = releasePublicDeal(released, seats, 'c', true);
|
||
assert.equal(released.status, 'released');
|
||
assert.equal(released.paymentStatus, 'paid');
|
||
validatePublicDeals([{ ...offered, id: 'offered' }, active, { ...released, id: 'released' }], seats);
|
||
});
|
||
|
||
void test('mutual release and proven impossibility end only an unpaid accepted payment', () => {
|
||
const first = accept('first', 4);
|
||
const vote = releasePublicDeal(first, seats, 'a', true);
|
||
assert.equal(vote.paymentStatus, 'pending');
|
||
const released = releasePublicDeal(vote, seats, 'b', true);
|
||
assert.equal(released.paymentStatus, 'released');
|
||
const impossible = voidPublicDealPayment(first, seats);
|
||
assert.equal(impossible.paymentStatus, 'void');
|
||
assert.throws(() => markPublicDealPaid(impossible, seats, 'a'), PublicDealError);
|
||
assert.throws(() => voidPublicDealPayment(released, seats), PublicDealError);
|
||
const bad = [
|
||
{ ...first, paymentStatus: undefined }, { ...first, paymentStatus: 'released' },
|
||
{ ...first, status: 'offered', paymentStatus: 'pending' },
|
||
{ ...first, payment: { ...deferred(4), dueTurn: 4 } },
|
||
{ ...first, payment: { kind: 'immediateBribe', payer: 'a', recipient: 'b', amount: 4 }, paymentStatus: 'paid' },
|
||
];
|
||
for (const deal of bad) assert.throws(() => validatePublicDeals([deal], seats), PublicDealError);
|
||
});
|
||
|
||
void test('distinct pending agreements add, voluntary spending is guarded, and uncertain external shortage does not erase them', () => {
|
||
const deals: PublicDeal[] = [accept('first', 4), accept('second', 5)];
|
||
const original = structuredClone(deals);
|
||
assert.deepEqual(pendingPublicDealPayments(deals, 'a', 3).map(row => [row.deal, row.amount]), [['first', 4], ['second', 5]]);
|
||
assert.equal(publicDealPaymentTotal(deals, 'a', 3), 9);
|
||
assert.equal(publicDealPaymentTotal(deals, 'b', 3), 0);
|
||
assert.equal(publicDealPaymentTotal(deals, 'a', 4), 0);
|
||
assert.throws(() => reconcilePublicDealPayments(deals, seats, 'a', 3,
|
||
{ available: 8, change: 'voluntary' }), PublicDealObligationError);
|
||
assert.deepEqual(reconcilePublicDealPayments(deals, seats, 'a', 3,
|
||
{ available: 8, change: 'external' }), deals, 'Current shortage does not prove impossibility.');
|
||
assert.deepEqual(reconcilePublicDealPayments(deals, seats, 'a', 3,
|
||
{ available: null, change: 'external' }), deals);
|
||
const reconciled = reconcilePublicDealPayments(deals, seats, 'a', 3,
|
||
{ available: null, change: 'external', impossibleDealIds: ['second'] });
|
||
assert.equal(reconciled[0].paymentStatus, 'pending');
|
||
assert.equal(reconciled[1].paymentStatus, 'void');
|
||
assert.equal(publicDealPaymentTotal(reconciled, 'a', 3), 4);
|
||
assert.throws(() => reconcilePublicDealPayments(deals, seats, 'a', 3,
|
||
{ available: 9, change: 'voluntary', impossibleDealIds: ['second'] }), PublicDealObligationError);
|
||
assert.throws(() => reconcilePublicDealPayments(deals, seats, 'a', 3,
|
||
{ available: 9, change: 'external', impossibleDealIds: ['other'] }), PublicDealObligationError);
|
||
assert.deepEqual(deals, original, 'Rejected quotes and pure transitions preserve the caller’s state.');
|
||
});
|
||
|
||
void test('individually valid payments cannot overflow their combined obligation', () => {
|
||
const deals = [accept('large', Number.MAX_SAFE_INTEGER), accept('one', 1)];
|
||
assert.throws(() => publicDealPaymentTotal(deals, 'a', 3), PublicDealObligationError);
|
||
});
|