102 lines
6.8 KiB
TypeScript
102 lines
6.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { reconcileCoalitionEscrow, setCoalitionPledge, type CoalitionEscrowState,
|
|
type CoalitionEscrowMembership } from '../game/coalition-escrow';
|
|
import type { CoalitionState } from '../game/coalition-alliance';
|
|
|
|
const empty: CoalitionEscrowState = { authorizations: [], pledges: [], usedEpochs: [] };
|
|
const seats = (values: [string, string | null][]) => values.map(([id, ally]) => ({id, ally}));
|
|
const unallied: CoalitionEscrowMembership = { seats: seats([['a', null], ['b', null], ['c', null]]) };
|
|
const pair: CoalitionEscrowMembership = { seats: seats([['a', 'b'], ['b', 'a'], ['c', null]]) };
|
|
const bcPair: CoalitionEscrowMembership = { seats: seats([['a', null], ['b', 'c'], ['c', 'b']]) };
|
|
const tripleState: CoalitionState = {version: 1, revision: 1, proposal: null, usedEvents: [], groups: [{
|
|
id: 'abc', members: ['a', 'b', 'c'], formedTurn: 2, edges: [
|
|
{members: ['a', 'b'], formedTurn: 2}, {members: ['a', 'c'], formedTurn: 3},
|
|
{members: ['b', 'c'], formedTurn: 3},
|
|
],
|
|
}]};
|
|
const triple: CoalitionEscrowMembership = {
|
|
seats: seats([['a', null], ['b', null], ['c', null]]), coalitionState: tripleState,
|
|
};
|
|
const ab = {members: ['a', 'b'] as const, epoch: 'ab1'};
|
|
const ac = {members: ['a', 'c'] as const, epoch: 'ac1'};
|
|
const bc = {members: ['b', 'c'] as const, epoch: 'bc1'};
|
|
|
|
function startedPair() {
|
|
return reconcileCoalitionEscrow({before: unallied, after: pair, escrow: empty, newAuthorizations: [ab]}).escrow;
|
|
}
|
|
function startedTriple() {
|
|
return reconcileCoalitionEscrow({before: pair, after: triple, escrow: startedPair(), newAuthorizations: [ac, bc]}).escrow;
|
|
}
|
|
|
|
void test('each new edge receives its own epoch while the existing edge retains its epoch', () => {
|
|
const first = startedPair(), original = structuredClone(first);
|
|
const result = reconcileCoalitionEscrow({before: pair, after: triple, escrow: first, newAuthorizations: [ac, bc]});
|
|
assert.deepEqual(result.escrow.authorizations, [ab, ac, bc]);
|
|
assert.deepEqual(result.escrow.usedEpochs, ['ab1', 'ac1', 'bc1']);
|
|
assert.deepEqual(result.refunds, []);
|
|
assert.deepEqual(first, original);
|
|
assert.deepEqual(JSON.parse(JSON.stringify(result)), result);
|
|
assert.throws(() => reconcileCoalitionEscrow({before: pair, after: triple, escrow: first, newAuthorizations: [ac]}), /fresh epoch/);
|
|
assert.throws(() => reconcileCoalitionEscrow({before: pair, after: triple, escrow: first, newAuthorizations: [ac, ac, bc]}), /uniquely/);
|
|
assert.throws(() => reconcileCoalitionEscrow({before: pair, after: triple, escrow: first,
|
|
newAuthorizations: [{...ac, epoch: 'ab1'}, bc]}), /reused/);
|
|
});
|
|
|
|
void test('recipient-specific pledges retain independent balances and protect committed spice', () => {
|
|
const initial = startedTriple(), before = structuredClone(initial);
|
|
const one = setCoalitionPledge({...triple, escrow: initial, donor: 'a', recipient: 'b', amount: 6, available: 10});
|
|
assert.equal(one.walletDelta, -6);
|
|
const two = setCoalitionPledge({...triple, escrow: one.escrow, donor: 'a', recipient: 'c', amount: 3, available: 4});
|
|
assert.equal(two.walletDelta, -3);
|
|
const reduced = setCoalitionPledge({...triple, escrow: two.escrow, donor: 'a', recipient: 'b', amount: 4, available: 1, locked: 4});
|
|
assert.equal(reduced.walletDelta, 2);
|
|
assert.deepEqual(reduced.escrow.pledges, [
|
|
{donor: 'a', recipient: 'c', amount: 3, epoch: 'ac1'},
|
|
{donor: 'a', recipient: 'b', amount: 4, epoch: 'ab1'},
|
|
]);
|
|
assert.throws(() => setCoalitionPledge({...triple, escrow: two.escrow, donor: 'a', recipient: 'b', amount: 3, available: 1, locked: 4}), /Committed/);
|
|
assert.throws(() => setCoalitionPledge({...triple, escrow: two.escrow, donor: 'a', recipient: 'b', amount: 8, available: 1}), /lacks/);
|
|
assert.throws(() => setCoalitionPledge({...triple, escrow: two.escrow, donor: 'a', recipient: 'a', amount: 1, available: 1}), /valid donor/);
|
|
assert.deepEqual(initial, before);
|
|
});
|
|
|
|
void test('a departing member receives each refund and surviving allies keep their edge and escrow', () => {
|
|
let ledger = startedTriple();
|
|
for (const [donor, recipient, amount] of [['a', 'b', 3], ['a', 'c', 4], ['b', 'c', 5], ['c', 'a', 2]] as const)
|
|
ledger = setCoalitionPledge({...triple, escrow: ledger, donor, recipient, amount, available: 20}).escrow;
|
|
const before = structuredClone(ledger);
|
|
const result = reconcileCoalitionEscrow({before: triple, after: bcPair, escrow: ledger, newAuthorizations: []});
|
|
assert.deepEqual(result.escrow.authorizations, [bc]);
|
|
assert.deepEqual(result.escrow.pledges, [{donor: 'b', recipient: 'c', amount: 5, epoch: 'bc1'}]);
|
|
assert.deepEqual(result.refunds, [{donor: 'a', amount: 7}, {donor: 'c', amount: 2}]);
|
|
assert.deepEqual(ledger, before);
|
|
assert.throws(() => reconcileCoalitionEscrow({before: triple, after: bcPair, escrow: ledger, newAuthorizations: [],
|
|
locked: [{donor: 'a', recipient: 'b', amount: 1, epoch: 'ab1'}]}), /Resolve committed/);
|
|
});
|
|
|
|
void test('same-turn reformation requires fresh epochs and cannot reactivate old authorization', () => {
|
|
const old = startedTriple();
|
|
const left = reconcileCoalitionEscrow({before: triple, after: bcPair, escrow: old, newAuthorizations: []}).escrow;
|
|
assert.throws(() => reconcileCoalitionEscrow({before: bcPair, after: triple, escrow: left, newAuthorizations: [ab, ac]}), /reused/);
|
|
const result = reconcileCoalitionEscrow({before: bcPair, after: triple, escrow: left,
|
|
newAuthorizations: [{...ab, epoch: 'ab2'}, {...ac, epoch: 'ac2'}]});
|
|
assert.deepEqual(result.escrow.authorizations, [{...ab, epoch: 'ab2'}, {...ac, epoch: 'ac2'}, bc]);
|
|
assert.deepEqual(result.escrow.usedEpochs, ['ab1', 'ac1', 'bc1', 'ab2', 'ac2']);
|
|
});
|
|
|
|
void test('overflow, duplicate or malformed ledgers and forged locks fail without mutation', () => {
|
|
const huge: CoalitionEscrowState = {...startedTriple(), pledges: [
|
|
{donor: 'a', recipient: 'b', amount: Number.MAX_SAFE_INTEGER, epoch: 'ab1'},
|
|
{donor: 'a', recipient: 'c', amount: 1, epoch: 'ac1'},
|
|
]};
|
|
const before = structuredClone(huge);
|
|
assert.throws(() => reconcileCoalitionEscrow({before: triple, after: bcPair, escrow: huge, newAuthorizations: []}), /safe integers/);
|
|
assert.throws(() => setCoalitionPledge({...triple, escrow: huge, donor: 'a', recipient: 'c', amount: 0, available: Number.MAX_SAFE_INTEGER}), /safe integers/);
|
|
assert.throws(() => reconcileCoalitionEscrow({before: triple, after: triple, escrow: huge, newAuthorizations: [],
|
|
locked: [{donor: 'a', recipient: 'c', amount: 2, epoch: 'ac1'}]}), /match its available/);
|
|
assert.throws(() => reconcileCoalitionEscrow({before: triple, after: triple, escrow: {...huge, usedEpochs: ['ab1', 'ac1', 'ac1', 'bc1']}, newAuthorizations: []}), /unique valid/);
|
|
assert.throws(() => setCoalitionPledge({...triple, escrow: {...huge, pledges: [...huge.pledges, huge.pledges[0]]}, donor: 'a', recipient: 'b', amount: 1, available: 1}), /only one pledge/);
|
|
assert.deepEqual(huge, before);
|
|
});
|