import { describe, expect, it } from 'vitest'; import { summarizeNationPeriod, projectCurrentSettlement, zAuditNation, type AuditNationData, type NationMonthPoint, } from '../src/router/playAudit/nationSeries.js'; const population = { count: 0, crew: null, gold: 0, rice: 0, dex: { dex1: 0, dex2: 0, dex3: 0, dex4: 0, dex5: 0 }, averageGold: null, averageRice: null, averageDex: { dex1: null, dex2: null, dex3: null, dex4: null, dex5: null }, }; const nation = (month: number): AuditNationData => ({ id: 1, name: `국가${month}`, color: '#ffffff', gold: month * 100, rice: month * 200, tech: month * 10, appliedRate: 20, incomeGold: month, incomeRice: 0, paidGold: month / 2, paidRice: 0, populations: { human: population, npc: population, troopNpc: population }, }); const months: NationMonthPoint[] = Array.from({ length: 6 }, (_, index) => ({ ordinal: 2400 + index, collected: true, settlementsComplete: true, data: nation(index + 1), })); describe('play audit half-year summary', () => { it('sums flows but takes stock, names and population denominators from the last month', () => { const summary = summarizeNationPeriod(2400, 6, months); expect(summary).toMatchObject({ year: 200, month: 1, complete: true, stockAsOf: { year: 200, month: 6 }, stock: { name: '국가6', gold: 600, rice: 1200, tech: 60, populations: { human: { count: 0, averageGold: null } }, }, flows: { incomeGold: 21, incomeRice: 0, paidGold: 10.5, paidRice: 0 }, }); }); it('reports partial range independently from complete collection of the requested months', () => { expect(summarizeNationPeriod(2400, 6, months.slice(0, 2))).toMatchObject({ complete: false, from: { year: 200, month: 1 }, to: { year: 200, month: 2 }, flows: { incomeGold: 3 }, }); }); it('does not call a missing month or absent nation zero income', () => { const missing = months.map((point, index) => index === 1 ? { ...point, collected: false, data: null } : point ); expect(summarizeNationPeriod(2400, 6, missing)).toMatchObject({ complete: false, flows: { incomeGold: null } }); expect( summarizeNationPeriod( 2400, 6, months.map((point) => ({ ...point, data: null })) ) ).toMatchObject({ complete: false, stock: null, stockAsOf: null, flows: { incomeRice: null }, }); }); it('keeps incomplete settlement coverage unknown while preserving stock coverage', () => { const partial = months.map((point, index) => ({ ...point, settlementsComplete: index !== 0 })); expect(summarizeNationPeriod(2400, 6, partial)).toMatchObject({ complete: true, flows: { incomeGold: null } }); expect(summarizeNationPeriod(2400, 6, [])).toMatchObject({ complete: false, stock: null, flows: { incomeGold: null }, }); }); }); describe('current committed settlement', () => { it.each([ [1, 'gold'], [7, 'rice'], ] as const)('exposes month %s settlement before month-end', (month, resource) => { const meta = { playAuditFlows: { year: 200, month, complete: true, entries: { [`2:${resource}`]: { nationId: 2, resource, income: 1234.5, paid: 234 } }, }, }; expect(projectCurrentSettlement(meta, 200, month, 2)[resource]).toEqual({ income: 1234.5, paid: 234 }); expect(projectCurrentSettlement(meta, 200, month + 1, 2)[resource]).toBeNull(); expect(projectCurrentSettlement(meta, 200, month, 3)[resource]).toBeNull(); }); it('keeps absent and malformed observations unknown, preserves actual zero', () => { expect(projectCurrentSettlement({}, 200, 1, 2).gold).toBeNull(); const meta = { playAuditFlows: { year: 200, month: 1, complete: false, entries: { '2:gold': { nationId: 2, resource: 'gold', income: 0, paid: 0 } }, }, }; expect(projectCurrentSettlement(meta, 200, 1, 2)).toMatchObject({ gold: { income: 0, paid: 0 }, complete: false, }); meta.playAuditFlows.entries['2:gold'].income = NaN; expect(projectCurrentSettlement(meta, 200, 1, 2).gold).toBeNull(); }); it('reads old snapshots without inventing troop counts', () => { const old = JSON.parse(JSON.stringify(nation(1))); delete old.populations.human.crew; expect(zAuditNation.parse(old).populations.human.crew).toBeNull(); }); });