import test from 'node:test'; import assert from 'node:assert/strict'; import { quoteCoalitionFunding, reconcileCoalitionPledges, settleCoalitionFunding, type CoalitionFundingInput } from '../game/coalition-funding'; import type { CoalitionSeat, CoalitionState } from '../game/coalition-alliance'; function fixture(): CoalitionFundingInput { const seats: CoalitionSeat[] = ['a', 'b', 'c', 'd'].map(id => ({ id, ally: null })); const coalitionState: CoalitionState = { version: 1, revision: 3, 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 }, ], }] }; return { seats, coalitionState, purpose: 'bid', payer: 'a', cost: 10, ownAvailable: 4, authorizations: [{ members: ['a', 'b'], epoch: 'ab1' }, { members: ['a', 'c'], epoch: 'ac1' }, { members: ['b', 'c'], epoch: 'bc1' }], pledges: [ { donor: 'b', recipient: 'a', amount: 4, epoch: 'ab1' }, { donor: 'c', recipient: 'a', amount: 5, epoch: 'ac1' }, { donor: 'b', recipient: 'c', amount: 2, epoch: 'bc1' }, ], contributions: [{ donor: 'b', amount: 3 }, { donor: 'c', amount: 4 }] }; } void test('exact split uses each donor escrow and preserves unrelated pledges', () => { const input = fixture(), original = structuredClone(input); const quote = quoteCoalitionFunding(input); assert.deepEqual(quote, { purpose: 'bid', payer: 'a', cost: 10, own: 3, contributions: [{ donor: 'b', amount: 3 }, { donor: 'c', amount: 4 }] }); assert.deepEqual(settleCoalitionFunding(input), { quote, pledges: [ { donor: 'b', recipient: 'a', amount: 1, epoch: 'ab1' }, { donor: 'c', recipient: 'a', amount: 1, epoch: 'ac1' }, { donor: 'b', recipient: 'c', amount: 2, epoch: 'bc1' }, ] }); assert.deepEqual(input, original); }); void test('pair and own-only payments retain exact split including zero-cost shipments', () => { const seats: CoalitionSeat[] = [{ id: 'a', ally: 'b' }, { id: 'b', ally: 'a' }, { id: 'c', ally: null }]; const input: CoalitionFundingInput = { seats, purpose: 'shipment', payer: 'a', cost: 6, ownAvailable: 4, authorizations: [{ members: ['a', 'b'], epoch: 'ab1' }], pledges: [{ donor: 'b', recipient: 'a', amount: 3, epoch: 'ab1' }], contributions: [{ donor: 'b', amount: 2 }] }; assert.deepEqual(settleCoalitionFunding(input), { quote: { purpose: 'shipment', payer: 'a', cost: 6, own: 4, contributions: [{ donor: 'b', amount: 2 }] }, pledges: [{ donor: 'b', recipient: 'a', amount: 1, epoch: 'ab1' }] }); assert.equal(quoteCoalitionFunding({ ...input, cost: 4, contributions: [] }).own, 4); assert.equal(quoteCoalitionFunding({ ...input, cost: 0, ownAvailable: 0, contributions: [] }).own, 0); }); void test('settlement revalidates current pledges, coalition and payer budget', () => { const input = fixture(); quoteCoalitionFunding(input); assert.throws(() => settleCoalitionFunding({ ...input, ownAvailable: 2 }), /remaining cost/); assert.throws(() => settleCoalitionFunding({ ...input, pledges: [ { donor: 'b', recipient: 'a', amount: 2, epoch: 'ab1' }, ...input.pledges.slice(1), ] }), /current escrow/); const seats: CoalitionSeat[] = [{ id: 'a', ally: 'b' }, { id: 'b', ally: 'a' }, { id: 'c', ally: null }, { id: 'd', ally: null }]; assert.throws(() => settleCoalitionFunding({ ...input, seats, coalitionState: undefined, authorizations: [{ members: ['a', 'b'], epoch: 'ab1' }] }), /Reconcile and refund stale/); assert.throws(() => settleCoalitionFunding({ ...input, coalitionState: null as unknown as CoalitionState }), /ledger cannot be null/); }); void test('only the stated recipient can use a pledge and no split silently reallocates', () => { const input = fixture(); assert.throws(() => quoteCoalitionFunding({ ...input, contributions: [{ donor: 'd', amount: 1 }] }), /current escrow/); assert.throws(() => quoteCoalitionFunding({ ...input, payer: 'c', contributions: [{ donor: 'c', amount: 1 }] }), /current escrow/); assert.throws(() => quoteCoalitionFunding({ ...input, cost: 2 }), /exceed the cost/); assert.throws(() => quoteCoalitionFunding({ ...input, contributions: [{ donor: 'b', amount: 3 }, { donor: 'b', amount: 1 }] }), /each donating ally once/i); assert.throws(() => quoteCoalitionFunding({ ...input, contributions: [{ donor: 'b', amount: 0 }] }), /positive safe/); assert.throws(() => quoteCoalitionFunding({ ...input, pledges: [...input.pledges, input.pledges[0]] }), /only one pledge/); assert.throws(() => quoteCoalitionFunding({ ...input, pledges: [{ donor: 'd', recipient: 'a', amount: 1, epoch: 'ad1' }] }), /Reconcile and refund stale/); }); void test('safe integer boundaries avoid overflow and JSON round trips keep the split', () => { const input = fixture(), max = Number.MAX_SAFE_INTEGER; const large: CoalitionFundingInput = { ...input, cost: max, ownAvailable: 0, pledges: [{ donor: 'b', recipient: 'a', amount: max, epoch: 'ab1' }, { donor: 'c', recipient: 'a', amount: max, epoch: 'ac1' }], contributions: [{ donor: 'b', amount: max }] }; assert.equal(settleCoalitionFunding(large).quote.own, 0); assert.deepEqual(settleCoalitionFunding(JSON.parse(JSON.stringify(input)) as CoalitionFundingInput), settleCoalitionFunding(input)); for (const bad of [-1, 0.5, NaN, Infinity, max + 1]) { assert.throws(() => quoteCoalitionFunding({ ...input, cost: bad }), /safe spice/); assert.throws(() => quoteCoalitionFunding({ ...input, contributions: [{ donor: 'b', amount: bad }] }), /positive safe/); assert.throws(() => quoteCoalitionFunding({ ...input, pledges: [{ donor: 'b', recipient: 'a', amount: bad, epoch: 'ab1' }] }), /safe escrow/); } }); void test('a broken and re-formed alliance cannot spend an old pledge even in the same turn', () => { const seats: CoalitionSeat[] = [{ id: 'a', ally: 'b' }, { id: 'b', ally: 'a' }, { id: 'c', ally: null }]; const original: CoalitionFundingInput = { seats, purpose: 'bid', payer: 'a', cost: 4, ownAvailable: 0, authorizations: [{ members: ['a', 'b'], epoch: 'first-edge' }], pledges: [{ donor: 'b', recipient: 'a', amount: 4, epoch: 'first-edge' }], contributions: [{ donor: 'b', amount: 4 }] }; assert.equal(quoteCoalitionFunding(original).own, 0); const broken = { ...original, seats: seats.map(seat => ({ ...seat, ally: null })), authorizations: [] }; assert.deepEqual(reconcileCoalitionPledges(broken), { pledges: [], refunds: original.pledges }); const reformed = { ...original, authorizations: [{ members: ['a', 'b'] as [string, string], epoch: 'new-edge' }] }; assert.throws(() => settleCoalitionFunding(reformed), /Reconcile and refund stale/); assert.deepEqual(reconcileCoalitionPledges(reformed), { pledges: [], refunds: original.pledges }); const fresh = { ...reformed, pledges: [{ donor: 'b', recipient: 'a', amount: 4, epoch: 'new-edge' }] }; assert.equal(settleCoalitionFunding(fresh).pledges[0].amount, 0); }); void test('unrelated stale pledges require explicit reconciliation without consuming valid escrow', () => { const input = fixture(); const changed = { ...input, authorizations: [input.authorizations[0], input.authorizations[1], { members: ['b', 'c'] as [string, string], epoch: 'bc2' }] }; assert.throws(() => quoteCoalitionFunding(changed), /Reconcile and refund stale/); const reconciled = reconcileCoalitionPledges(changed); assert.deepEqual(reconciled.refunds, [input.pledges[2]]); assert.deepEqual(reconciled.pledges, input.pledges.slice(0, 2)); assert.equal(settleCoalitionFunding({ ...changed, pledges: reconciled.pledges }).quote.own, 3); assert.deepEqual(input.pledges, fixture().pledges); }); void test('authorizations must cover exactly the current edges with valid ordered epochs', () => { const input = fixture(); assert.throws(() => quoteCoalitionFunding({ ...input, authorizations: input.authorizations.slice(1) }), /Every current alliance edge/); assert.throws(() => quoteCoalitionFunding({ ...input, authorizations: [...input.authorizations, input.authorizations[0]] }), /exactly/); assert.throws(() => quoteCoalitionFunding({ ...input, authorizations: [input.authorizations[0], { members: ['a', 'c'], epoch: 'ab1' }, input.authorizations[2]] }), /uniquely/); assert.throws(() => quoteCoalitionFunding({ ...input, authorizations: [{ members: ['b', 'a'], epoch: 'bad' }] }), /ordered pair/); assert.throws(() => quoteCoalitionFunding({ ...input, authorizations: [{ members: ['a', 'b'], epoch: ' ' }] }), /ordered pair/); assert.throws(() => quoteCoalitionFunding({ ...input, pledges: [{ donor: 'b', recipient: 'a', amount: 1, epoch: '' }] }), /epoch/); });