Files
Dune/tests/public-deal-deferred.test.ts
T

99 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});