315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { HomeworldCustodyError } from '../game/homeworld-custody';
|
|
import { quoteHomeworldBankPayment } from '../game/homeworld-payment-income';
|
|
|
|
void test('whole-invoice low Bank halves attribute odd-odd rounding to the declared primary payer', () => {
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(
|
|
[{ payer: 'primary', amount: 5 }, { payer: 'ally', amount: 3 }],
|
|
4,
|
|
),
|
|
[{ payer: 'primary', amount: 3 }, { payer: 'ally', amount: 1 }],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(
|
|
[{ payer: 'primary', amount: 7 }, { payer: 'ally', amount: 1 }],
|
|
4,
|
|
),
|
|
[{ payer: 'primary', amount: 4 }],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(
|
|
[{ payer: 'ally', amount: 3 }, { payer: 'primary', amount: 5 }],
|
|
4,
|
|
),
|
|
[{ payer: 'ally', amount: 2 }, { payer: 'primary', amount: 2 }],
|
|
);
|
|
});
|
|
|
|
void test('native Bank zero and canceled full income preserve the actual original payer legs', () => {
|
|
const contributions = [
|
|
{ payer: 'primary', amount: 5 },
|
|
{ payer: 'ally', amount: 3 },
|
|
];
|
|
assert.deepEqual(quoteHomeworldBankPayment(contributions, 0), []);
|
|
assert.deepEqual(quoteHomeworldBankPayment(contributions, 8), contributions);
|
|
assert.deepEqual(quoteHomeworldBankPayment([], 0), []);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment([{ payer: 'primary', amount: 1 }], 0),
|
|
[],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment([{ payer: 'primary', amount: 1 }], 1),
|
|
[{ payer: 'primary', amount: 1 }],
|
|
);
|
|
});
|
|
|
|
void test('occupied cancellation uses the frozen ceil Bank remainder rather than the ordinary low floor', () => {
|
|
const contributions = [
|
|
{ payer: 'primary', amount: 5 },
|
|
{ payer: 'ally', amount: 4 },
|
|
];
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, 4),
|
|
[{ payer: 'primary', amount: 2 }, { payer: 'ally', amount: 2 }],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, 5),
|
|
[{ payer: 'primary', amount: 3 }, { payer: 'ally', amount: 2 }],
|
|
);
|
|
assert.deepEqual(quoteHomeworldBankPayment(contributions, 0), []);
|
|
});
|
|
|
|
void test('Bureaucrat combines its diversion with the same payer final Bank half, never another payer', () => {
|
|
const bankLegs = quoteHomeworldBankPayment(
|
|
[{ payer: 'primary', amount: 5 }, { payer: 'ally', amount: 3 }],
|
|
3,
|
|
{ payer: 'primary', amount: 2 },
|
|
);
|
|
assert.deepEqual(bankLegs, [
|
|
{ payer: 'primary', amount: 4 },
|
|
{ payer: 'ally', amount: 1 },
|
|
]);
|
|
assert.deepEqual(
|
|
bankLegs.filter((leg) => leg.amount >= 4).map((leg) => leg.payer),
|
|
['primary'],
|
|
);
|
|
});
|
|
|
|
void test('two qualifying Bureaucrat contributions use the selected first eligible payer in original order', () => {
|
|
const firstFive = quoteHomeworldBankPayment(
|
|
[{ payer: 'primary', amount: 5 }, { payer: 'ally', amount: 6 }],
|
|
4,
|
|
{ payer: 'primary', amount: 2 },
|
|
);
|
|
assert.deepEqual(firstFive, [
|
|
{ payer: 'primary', amount: 3 },
|
|
{ payer: 'ally', amount: 3 },
|
|
]);
|
|
assert.deepEqual(firstFive.filter((leg) => leg.amount >= 4), []);
|
|
const firstSix = quoteHomeworldBankPayment(
|
|
[{ payer: 'primary', amount: 6 }, { payer: 'ally', amount: 5 }],
|
|
4,
|
|
{ payer: 'primary', amount: 2 },
|
|
);
|
|
assert.deepEqual(firstSix, [
|
|
{ payer: 'primary', amount: 4 },
|
|
{ payer: 'ally', amount: 2 },
|
|
]);
|
|
assert.deepEqual(
|
|
firstSix.filter((leg) => leg.amount >= 4).map((leg) => leg.payer),
|
|
['primary'],
|
|
);
|
|
});
|
|
|
|
void test('Bureaucrat can select the ally when the declared primary contribution is below five', () => {
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(
|
|
[{ payer: 'primary', amount: 4 }, { payer: 'ally', amount: 5 }],
|
|
3,
|
|
{ payer: 'ally', amount: 2 },
|
|
),
|
|
[{ payer: 'primary', amount: 2 }, { payer: 'ally', amount: 3 }],
|
|
);
|
|
});
|
|
|
|
void test('diversion is excluded before zero, full, floor and occupied ceil Bank allocation', () => {
|
|
const contributions = [
|
|
{ payer: 'primary', amount: 5 },
|
|
{ payer: 'ally', amount: 4 },
|
|
];
|
|
const diversion = { payer: 'primary', amount: 2 } as const;
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, 0, diversion),
|
|
[{ payer: 'primary', amount: 2 }],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, 7, diversion),
|
|
[{ payer: 'primary', amount: 5 }, { payer: 'ally', amount: 4 }],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, 3, diversion),
|
|
[{ payer: 'primary', amount: 3 }, { payer: 'ally', amount: 2 }],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, 4, diversion),
|
|
[{ payer: 'primary', amount: 4 }, { payer: 'ally', amount: 2 }],
|
|
);
|
|
});
|
|
|
|
void test('distinct payer and distinct invoice Bank payments are not pooled into a Banker threshold', () => {
|
|
const funded = quoteHomeworldBankPayment(
|
|
[{ payer: 'primary', amount: 5 }, { payer: 'ally', amount: 3 }],
|
|
4,
|
|
);
|
|
assert.deepEqual(funded, [
|
|
{ payer: 'primary', amount: 3 },
|
|
{ payer: 'ally', amount: 1 },
|
|
]);
|
|
assert.deepEqual(funded.filter((leg) => leg.amount >= 4), []);
|
|
for (let invoice = 0; invoice < 2; invoice++) {
|
|
const bankLegs = quoteHomeworldBankPayment(
|
|
[{ payer: 'primary', amount: 5 }],
|
|
2,
|
|
);
|
|
assert.deepEqual(bankLegs, [{ payer: 'primary', amount: 2 }]);
|
|
assert.deepEqual(bankLegs.filter((leg) => leg.amount >= 4), []);
|
|
}
|
|
});
|
|
|
|
void test('all valid final Bank modes conserve each original contribution across odd and even funded payments', () => {
|
|
for (let primary = 1; primary <= 11; primary++)
|
|
for (let ally = 1; ally <= 11; ally++) {
|
|
const contributions = [
|
|
{ payer: 'primary', amount: primary },
|
|
{ payer: 'ally', amount: ally },
|
|
];
|
|
const diversions = [
|
|
undefined,
|
|
...(primary >= 5 ? [{ payer: 'primary', amount: 2 } as const] : []),
|
|
...(ally >= 5 ? [{ payer: 'ally', amount: 2 } as const] : []),
|
|
];
|
|
for (const diversion of diversions) {
|
|
const remaining = primary + ally - (diversion ? 2 : 0);
|
|
for (const bank of [0, remaining, Math.floor(remaining / 2), Math.ceil(remaining / 2)]) {
|
|
const legs = quoteHomeworldBankPayment(contributions, bank, diversion);
|
|
assert.equal(
|
|
legs.reduce((sum, leg) => sum + leg.amount, 0),
|
|
bank + (diversion ? 2 : 0),
|
|
);
|
|
for (const leg of legs) {
|
|
const original = contributions.find((paid) => paid.payer === leg.payer)!;
|
|
assert.ok(Number.isSafeInteger(leg.amount) && leg.amount > 0);
|
|
assert.ok(leg.amount <= original.amount);
|
|
assert.equal(legs.filter((paid) => paid.payer === leg.payer).length, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('Bank attribution preserves exact maximal safe payments and their half rounding', () => {
|
|
const contributions = [{ payer: 'primary', amount: Number.MAX_SAFE_INTEGER }];
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, 4_503_599_627_370_495),
|
|
[{ payer: 'primary', amount: 4_503_599_627_370_495 }],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, 4_503_599_627_370_496),
|
|
[{ payer: 'primary', amount: 4_503_599_627_370_496 }],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, 4_503_599_627_370_494, { payer: 'primary', amount: 2 }),
|
|
[{ payer: 'primary', amount: 4_503_599_627_370_496 }],
|
|
);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(contributions, Number.MAX_SAFE_INTEGER),
|
|
contributions,
|
|
);
|
|
});
|
|
|
|
void test('Bank allocation survives JSON and frozen inputs without changing original payments or diversion', () => {
|
|
const contributions = Object.freeze([
|
|
Object.freeze({ payer: 'primary', amount: 5 }),
|
|
Object.freeze({ payer: 'ally', amount: 3 }),
|
|
]);
|
|
const diversion = Object.freeze({ payer: 'primary', amount: 2 } as const);
|
|
const before = JSON.stringify({ contributions, diversion });
|
|
const legs = quoteHomeworldBankPayment(contributions, 3, diversion);
|
|
assert.deepEqual(legs, [
|
|
{ payer: 'primary', amount: 4 },
|
|
{ payer: 'ally', amount: 1 },
|
|
]);
|
|
assert.deepEqual(
|
|
quoteHomeworldBankPayment(JSON.parse(JSON.stringify(contributions)), 3, JSON.parse(JSON.stringify(diversion))),
|
|
legs,
|
|
);
|
|
legs[0].amount = 1;
|
|
assert.equal(JSON.stringify({ contributions, diversion }), before);
|
|
});
|
|
|
|
void test('invalid original payer attribution, contribution amounts and frozen Bank modes throw typed errors without mutation', () => {
|
|
const valid = [{ payer: 'primary', amount: 5 }, { payer: 'ally', amount: 3 }];
|
|
const invalid: [unknown, unknown][] = [
|
|
[undefined, 0],
|
|
[null, 0],
|
|
[{}, 0],
|
|
['primary', 0],
|
|
[Array(1), 0],
|
|
[[null], 0],
|
|
[[{}], 0],
|
|
[[{ payer: '', amount: 5 }], 2],
|
|
[[{ payer: ' ', amount: 5 }], 2],
|
|
[[{ payer: 1, amount: 5 }], 2],
|
|
[[{ payer: 'primary', amount: 0 }], 0],
|
|
[[{ payer: 'primary', amount: -1 }], 0],
|
|
[[{ payer: 'primary', amount: NaN }], 0],
|
|
[[{ payer: 'primary', amount: Infinity }], 0],
|
|
[[{ payer: 'primary', amount: 1.5 }], 0],
|
|
[[{ payer: 'primary', amount: '5' }], 2],
|
|
[[{ payer: 'primary', amount: Number.MAX_SAFE_INTEGER + 1 }], 0],
|
|
[[{ payer: 'primary', amount: Number.MAX_SAFE_INTEGER }, { payer: 'ally', amount: 1 }], 0],
|
|
[[{ payer: 'primary', amount: 5 }, { payer: 'primary', amount: 3 }], 4],
|
|
[valid, -1],
|
|
[valid, 1.5],
|
|
[valid, NaN],
|
|
[valid, Infinity],
|
|
[valid, '4'],
|
|
[valid, Number.MAX_SAFE_INTEGER + 1],
|
|
[valid, 3],
|
|
[valid, 9],
|
|
[[], 1],
|
|
];
|
|
for (const [contributions, bank] of invalid) {
|
|
const before = JSON.stringify(contributions);
|
|
assert.throws(
|
|
() => quoteHomeworldBankPayment(
|
|
contributions as Parameters<typeof quoteHomeworldBankPayment>[0],
|
|
bank as number,
|
|
),
|
|
HomeworldCustodyError,
|
|
);
|
|
assert.equal(JSON.stringify(contributions), before);
|
|
}
|
|
});
|
|
|
|
void test('invalid Bureaucrat selection or diversion amount rejects without changing the invoice', () => {
|
|
const contributions = Object.freeze([
|
|
Object.freeze({ payer: 'primary', amount: 4 }),
|
|
Object.freeze({ payer: 'ally', amount: 5 }),
|
|
]);
|
|
const invalid: unknown[] = [
|
|
null,
|
|
{},
|
|
{ payer: 'absent', amount: 2 },
|
|
{ payer: 'primary', amount: 2 },
|
|
{ payer: 'ally', amount: 0 },
|
|
{ payer: 'ally', amount: 1 },
|
|
{ payer: 'ally', amount: 3 },
|
|
{ payer: 'ally', amount: 2.5 },
|
|
{ payer: 'ally', amount: '2' },
|
|
{ payer: 1, amount: 2 },
|
|
];
|
|
for (const diversion of invalid) {
|
|
const before = JSON.stringify({ contributions, diversion });
|
|
assert.throws(
|
|
() => quoteHomeworldBankPayment(
|
|
contributions,
|
|
3,
|
|
diversion as Parameters<typeof quoteHomeworldBankPayment>[2],
|
|
),
|
|
HomeworldCustodyError,
|
|
);
|
|
assert.equal(JSON.stringify({ contributions, diversion }), before);
|
|
}
|
|
assert.throws(
|
|
() => quoteHomeworldBankPayment(contributions, 5, { payer: 'ally', amount: 2 }),
|
|
HomeworldCustodyError,
|
|
);
|
|
assert.throws(
|
|
() => quoteHomeworldBankPayment([], 0, { payer: 'ally', amount: 2 }),
|
|
HomeworldCustodyError,
|
|
);
|
|
});
|