import assert from 'node:assert/strict'; import test from 'node:test'; import { PledgePromiseError, livePledgePromises, parsePledgeClaim, pledgePromiseRequirements, reconcilePledgePromises, recordPromisedPledge, validatePledgePromises, type PledgePromise, type PledgeReconciliation, } from '../game/pledge-promises'; const promise = (overrides: Partial = {}): PledgePromise => ({ turn: 2, phase: 3, ally: 'ally', minimum: 3, event: 'event-1', player: 'donor', asker: 'asker', answer: true, ...overrides, }); const context = (overrides: Partial = {}): PledgeReconciliation => ({ turn: 2, phase: 3, mutualAlly: player => player === 'donor' ? 'ally' : null, canFulfill: () => true, ...overrides, }); void test('a pledge question requires a current supported phase, current ally identity and positive safe threshold', () => { assert.deepEqual(parsePledgeClaim({turn: 2, phase: 5, ally: 'ally', minimum: 4}), {turn: 2, phase: 5, ally: 'ally', minimum: 4}); for (const value of [null, {}, {turn: 0, phase: 3, ally: 'ally', minimum: 1}, {turn: 2, phase: 4, ally: 'ally', minimum: 1}, {turn: 2, phase: 3, ally: ' ally ', minimum: 1}, {turn: 2, phase: 3, ally: 'ally', minimum: 0}, {turn: 2, phase: 3, ally: 'ally', minimum: Number.MAX_SAFE_INTEGER + 1}]) assert.throws(() => parsePledgeClaim(value), PledgePromiseError); }); void test('overlapping future Yes and No answers combine into a single bounded update', () => { const rows = [promise({minimum: 2}), promise({event: 'event-2', minimum: 4}), promise({event: 'event-3', answer: false, minimum: 6})]; assert.deepEqual(pledgePromiseRequirements(rows, 'donor', 2, 3, 'ally'), {ally: 'ally', minimum: 4, maximum: 5}); assert.equal(pledgePromiseRequirements([...rows, promise({event: 'event-4', answer: false, minimum: 4})], 'donor', 2, 3, 'ally'), null); assert.equal(pledgePromiseRequirements(rows, 'donor', 2, 3, 'different'), null); assert.deepEqual(pledgePromiseRequirements(rows, 'donor', 2, 5, 'ally'), {ally: 'ally', minimum: 0}); }); void test('only a future accepted native funding update satisfies Yes, including an explicit same-value update', () => { const rows = [promise({minimum: 3}), promise({event: 'event-2', minimum: 5}), promise({event: 'event-3', answer: false, minimum: 6})]; const before = structuredClone(rows); const low = recordPromisedPledge(rows, 'donor', 2, 3, {ally: 'ally', amount: 3}); assert.equal(low[0].fulfilled, true); assert.equal(low[1].fulfilled, undefined); assert.equal(low[2].fulfilled, undefined); assert.deepEqual(rows, before); const full = recordPromisedPledge(low, 'donor', 2, 3, {ally: 'ally', amount: 5}); assert.equal(full[1].fulfilled, true); assert.equal(full[2].fulfilled, undefined, 'No continues to restrict later same-phase updates'); assert.throws(() => recordPromisedPledge(full, 'donor', 2, 3, {ally: 'ally', amount: 6}), PledgePromiseError); assert.deepEqual(rows, before); assert.deepEqual(recordPromisedPledge(rows, 'donor', 2, 3, {ally: 'other', amount: 10}), rows); assert.deepEqual(recordPromisedPledge(rows, 'donor', 2, 5, {ally: 'ally', amount: 10}), rows); }); void test('an owner cannot voluntarily evade a feasible Yes while a foreign impossibility releases it', () => { const rows = [promise(), promise({event: 'no', answer: false, minimum: 5})]; const before = structuredClone(rows); const keep = reconcilePledgePromises(rows, context({canFulfill: (_row, quote) => { assert.deepEqual(quote, {ally: 'ally', minimum: 3, maximum: 4}); return true; }})); assert.deepEqual(keep, rows); assert.throws(() => reconcilePledgePromises(rows, context({canFulfill: () => false, voluntaryActor: 'donor'})), PledgePromiseError); assert.deepEqual(rows, before); const released = reconcilePledgePromises(rows, context({canFulfill: () => false, voluntaryActor: 'asker'})); assert.equal(released[0].released, true); assert.equal(released[1].fulfilled, undefined, 'The No survives a temporary affordability change'); assert.deepEqual(rows, before); }); void test('phase or reciprocal alliance changes expire No and release an unfulfilled Yes', () => { const rows = [promise(), promise({event: 'no', answer: false})]; for (const changed of [context({phase: 5}), context({turn: 3}), context({mutualAlly: () => null})]) { const result = reconcilePledgePromises(rows, changed); assert.equal(result[0].released, true); assert.equal(result[1].fulfilled, true); assert.deepEqual(livePledgePromises(result, 'donor', changed.turn, changed.phase), []); } assert.throws(() => reconcilePledgePromises(rows, context({phase: 5, voluntaryActor: 'donor'})), PledgePromiseError); assert.throws(() => reconcilePledgePromises(rows, context({mutualAlly: () => null, voluntaryActor: 'donor'})), PledgePromiseError); }); void test('saved rows retain distinct events, valid participants and noncontradictory terminal flags', () => { const players = ['donor', 'asker', 'ally']; assert.doesNotThrow(() => validatePledgePromises([promise()], players)); for (const bad of [ [promise(), promise()], [promise({player: 'ally'})], [promise({asker: 'donor'})], [promise({event: ''})], [promise({fulfilled: true, released: true})], [promise({answer: false, released: true})], ]) assert.throws(() => validatePledgePromises(bad, players), PledgePromiseError); });