test monthly catalog segment coverage
This commit is contained in:
@@ -20,6 +20,40 @@ export type MonthlyEventActionHandler = (
|
|||||||
|
|
||||||
export type MonthlyEventActionRegistry = ReadonlyMap<string, MonthlyEventActionHandler>;
|
export type MonthlyEventActionRegistry = ReadonlyMap<string, MonthlyEventActionHandler>;
|
||||||
|
|
||||||
|
export const MONTHLY_EVENT_ACTION_CATALOG = [
|
||||||
|
'ProcessIncome',
|
||||||
|
'NoticeToHistoryLog',
|
||||||
|
'NewYear',
|
||||||
|
'ResetOfficerLock',
|
||||||
|
'RandomizeCityTradeRate',
|
||||||
|
'RaiseDisaster',
|
||||||
|
'UpdateCitySupply',
|
||||||
|
'UpdateNationLevel',
|
||||||
|
'ProcessSemiAnnual',
|
||||||
|
'ProcessWarIncome',
|
||||||
|
'CreateAdminNPC',
|
||||||
|
'CreateManyNPC',
|
||||||
|
'RegNPC',
|
||||||
|
'RegNeutralNPC',
|
||||||
|
'RaiseNPCNation',
|
||||||
|
'RaiseInvader',
|
||||||
|
'AutoDeleteInvader',
|
||||||
|
'InvaderEnding',
|
||||||
|
'ChangeCity',
|
||||||
|
'ProvideNPCTroopLeader',
|
||||||
|
'OpenNationBetting',
|
||||||
|
'FinishNationBetting',
|
||||||
|
'BlockScoutAction',
|
||||||
|
'UnblockScoutAction',
|
||||||
|
'AssignGeneralSpeciality',
|
||||||
|
'AddGlobalBetray',
|
||||||
|
'LostUniqueItem',
|
||||||
|
'MergeInheritPointRank',
|
||||||
|
'DeleteEvent',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type MonthlyEventActionName = (typeof MONTHLY_EVENT_ACTION_CATALOG)[number];
|
||||||
|
|
||||||
const COMPARATORS = new Set(['==', '!=', '<', '>', '<=', '>=']);
|
const COMPARATORS = new Set(['==', '!=', '<', '>', '<=', '>=']);
|
||||||
const CITY_TRADE_PROBABILITY_BY_LEVEL: Readonly<Record<number, number>> = {
|
const CITY_TRADE_PROBABILITY_BY_LEVEL: Readonly<Record<number, number>> = {
|
||||||
1: 0,
|
1: 0,
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MONTHLY_EVENT_ACTION_CATALOG,
|
||||||
|
type MonthlyEventActionName,
|
||||||
|
} from '../src/turn/monthlyEventHandler.js';
|
||||||
|
|
||||||
|
interface CatalogSegment {
|
||||||
|
name: string;
|
||||||
|
kind: 'single-boundary' | 'multi-month' | 'no-op' | 'expected-failure';
|
||||||
|
actions: readonly MonthlyEventActionName[];
|
||||||
|
coreEvidence: readonly string[];
|
||||||
|
refEvidence: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = [
|
||||||
|
{
|
||||||
|
name: 'aggregate-core-boundary',
|
||||||
|
kind: 'single-boundary',
|
||||||
|
actions: [
|
||||||
|
'ProcessIncome',
|
||||||
|
'NoticeToHistoryLog',
|
||||||
|
'NewYear',
|
||||||
|
'ResetOfficerLock',
|
||||||
|
'RandomizeCityTradeRate',
|
||||||
|
'ChangeCity',
|
||||||
|
'DeleteEvent',
|
||||||
|
],
|
||||||
|
coreEvidence: ['monthlyCatalogBoundaryPersistence.integration.test.ts'],
|
||||||
|
refEvidence: ['monthly_boundary_core_catalog.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'city-economy-boundaries',
|
||||||
|
kind: 'single-boundary',
|
||||||
|
actions: [
|
||||||
|
'RaiseDisaster',
|
||||||
|
'UpdateCitySupply',
|
||||||
|
'UpdateNationLevel',
|
||||||
|
'ProcessSemiAnnual',
|
||||||
|
'ProcessWarIncome',
|
||||||
|
],
|
||||||
|
coreEvidence: [
|
||||||
|
'monthlyDisasterPersistence.integration.test.ts',
|
||||||
|
'monthlyCitySupplyPersistence.integration.test.ts',
|
||||||
|
'monthlyNationLevelPersistence.integration.test.ts',
|
||||||
|
'monthlySemiAnnualPersistence.integration.test.ts',
|
||||||
|
'monthlyWarIncomePersistence.integration.test.ts',
|
||||||
|
],
|
||||||
|
refEvidence: [
|
||||||
|
'raise_disaster fixed-seed trace',
|
||||||
|
'monthly_update_city_supply.json',
|
||||||
|
'monthly_update_nation_level.json',
|
||||||
|
'monthly_process_semi_annual.json',
|
||||||
|
'monthly_process_war_income.json',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'legacy-admin-npc-no-op',
|
||||||
|
kind: 'no-op',
|
||||||
|
actions: ['CreateAdminNPC'],
|
||||||
|
coreEvidence: ['monthlyCreateAdminNpcAction.test.ts'],
|
||||||
|
refEvidence: ['monthly_create_admin_npc.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'npc-registration-boundaries',
|
||||||
|
kind: 'single-boundary',
|
||||||
|
actions: ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseNPCNation'],
|
||||||
|
coreEvidence: [
|
||||||
|
'monthlyCreateManyNpcPersistence.integration.test.ts',
|
||||||
|
'monthlyRegisterNpcPersistence.integration.test.ts',
|
||||||
|
'monthlyRaiseNpcNationPersistence.integration.test.ts',
|
||||||
|
],
|
||||||
|
refEvidence: [
|
||||||
|
'monthly_create_many_npc.json',
|
||||||
|
'monthly_reg_npc.json',
|
||||||
|
'monthly_reg_neutral_npc.json',
|
||||||
|
'monthly_raise_npc_nation.json',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'invader-lifecycle',
|
||||||
|
kind: 'multi-month',
|
||||||
|
actions: ['RaiseInvader', 'AutoDeleteInvader', 'InvaderEnding'],
|
||||||
|
coreEvidence: ['monthlyInvaderPersistence.integration.test.ts'],
|
||||||
|
refEvidence: [
|
||||||
|
'monthly_raise_invader.json',
|
||||||
|
'monthly_auto_delete_invader.json',
|
||||||
|
'monthly_invader_ending.json',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'npc-troop-support',
|
||||||
|
kind: 'single-boundary',
|
||||||
|
actions: ['ProvideNPCTroopLeader'],
|
||||||
|
coreEvidence: ['monthlyNpcSupportPersistence.integration.test.ts'],
|
||||||
|
refEvidence: ['ProvideNPCTroopLeader fixed-seed trace'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nation-betting-lifecycle',
|
||||||
|
kind: 'multi-month',
|
||||||
|
actions: ['OpenNationBetting', 'FinishNationBetting'],
|
||||||
|
coreEvidence: ['monthlyNationBettingPersistence.integration.test.ts'],
|
||||||
|
refEvidence: ['monthly_open_nation_betting.json', 'monthly_finish_nation_betting.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'scout-block-boundaries',
|
||||||
|
kind: 'single-boundary',
|
||||||
|
actions: ['BlockScoutAction'],
|
||||||
|
coreEvidence: ['monthlyScoutBlockPersistence.integration.test.ts'],
|
||||||
|
refEvidence: ['monthly_block_scout.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'legacy-unblock-scout-error',
|
||||||
|
kind: 'expected-failure',
|
||||||
|
actions: ['UnblockScoutAction'],
|
||||||
|
coreEvidence: ['monthlyScoutBlockAction.test.ts'],
|
||||||
|
refEvidence: ['monthly_unblock_scout.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'speciality-betray-chain',
|
||||||
|
kind: 'single-boundary',
|
||||||
|
actions: ['AssignGeneralSpeciality', 'AddGlobalBetray'],
|
||||||
|
coreEvidence: ['monthlySpecialityBetrayPersistence.integration.test.ts'],
|
||||||
|
refEvidence: ['monthly_assign_general_speciality.json', 'monthly_add_global_betray.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unique-inherit-chain',
|
||||||
|
kind: 'single-boundary',
|
||||||
|
actions: ['LostUniqueItem', 'MergeInheritPointRank'],
|
||||||
|
coreEvidence: ['monthlyUniqueInheritPersistence.integration.test.ts'],
|
||||||
|
refEvidence: ['monthly_lost_unique_item.json', 'monthly_merge_inherit_point_rank.json'],
|
||||||
|
},
|
||||||
|
] as const satisfies readonly CatalogSegment[];
|
||||||
|
|
||||||
|
describe('monthly event catalog coverage', () => {
|
||||||
|
it('assigns every legacy action to exactly one dependency-safe segment', () => {
|
||||||
|
const covered = segments.flatMap((segment) => segment.actions);
|
||||||
|
|
||||||
|
expect(covered).toHaveLength(29);
|
||||||
|
expect(new Set(covered).size).toBe(29);
|
||||||
|
expect([...covered].sort()).toEqual([...MONTHLY_EVENT_ACTION_CATALOG].sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps every core evidence file executable in this suite', () => {
|
||||||
|
const testDirectory = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
const evidenceFiles = new Set(segments.flatMap((segment) => segment.coreEvidence));
|
||||||
|
|
||||||
|
for (const evidenceFile of evidenceFiles) {
|
||||||
|
expect(existsSync(`${testDirectory}/${evidenceFile}`), evidenceFile).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps lifecycle-only dispositions out of the aggregate single-boundary claim', () => {
|
||||||
|
const multiMonthActions = segments
|
||||||
|
.filter((segment) => segment.kind === 'multi-month')
|
||||||
|
.flatMap((segment) => segment.actions);
|
||||||
|
const specialDispositions = segments
|
||||||
|
.filter((segment) => segment.kind === 'no-op' || segment.kind === 'expected-failure')
|
||||||
|
.flatMap((segment) => segment.actions);
|
||||||
|
|
||||||
|
expect(multiMonthActions).toEqual([
|
||||||
|
'RaiseInvader',
|
||||||
|
'AutoDeleteInvader',
|
||||||
|
'InvaderEnding',
|
||||||
|
'OpenNationBetting',
|
||||||
|
'FinishNationBetting',
|
||||||
|
]);
|
||||||
|
expect(specialDispositions).toEqual(['CreateAdminNPC', 'UnblockScoutAction']);
|
||||||
|
expect(segments.every((segment) => segment.refEvidence.length > 0)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -67,6 +67,7 @@ environment-dependent check.
|
|||||||
| `test/databaseCommandQueue.integration.test.ts` | kept | integration | PostgreSQL single claim across consumers, persisted result, and expired-versus-active lease recovery. Explicitly skipped without a DB URL. |
|
| `test/databaseCommandQueue.integration.test.ts` | kept | integration | PostgreSQL single claim across consumers, persisted result, and expired-versus-active lease recovery. Explicitly skipped without a DB URL. |
|
||||||
| `test/generalAiLegacyDecisionParity.test.ts` | added | compatibility | Ref-backed final NPC command matrix across diplomacy, war readiness, city development/population, technology ceilings, gold/rice and casualty ranks, stats/affinity, special NPC state, treasury reserves, and command availability. |
|
| `test/generalAiLegacyDecisionParity.test.ts` | added | compatibility | Ref-backed final NPC command matrix across diplomacy, war readiness, city development/population, technology ceilings, gold/rice and casualty ranks, stats/affinity, special NPC state, treasury reserves, and command availability. |
|
||||||
| `test/inputEventAtomicity.test.ts` | kept | contract | Dirty-state retention, mutation/commit/respond ordering, and pause/no-ack behavior on handler or commit failure. |
|
| `test/inputEventAtomicity.test.ts` | kept | contract | Dirty-state retention, mutation/commit/respond ordering, and pause/no-ack behavior on handler or commit failure. |
|
||||||
|
| `test/monthlyCatalogCoverage.test.ts` | added | compatibility | Exact 29-action ref catalog coverage, dependency-safe segment assignment, executable core evidence files, and explicit multi-month/no-op/error dispositions. Behavioral claims remain in the referenced PostgreSQL/ref fixtures. |
|
||||||
| `test/nationCollapseOnConquest.test.ts` | kept | smoke | Last-city conquest removes the nation and neutralizes its general. It is a focused engine scenario, not full battle parity. |
|
| `test/nationCollapseOnConquest.test.ts` | kept | smoke | Last-city conquest removes the nation and neutralizes its general. It is a focused engine scenario, not full battle parity. |
|
||||||
| `test/nationTurnCompatibility.test.ts` | kept | compatibility | Legacy-backed 12-turn research accumulation and completion, diplomacy-message emission, and exact monthly strategy/diplomacy limit decay through the engine harness. |
|
| `test/nationTurnCompatibility.test.ts` | kept | compatibility | Legacy-backed 12-turn research accumulation and completion, diplomacy-message emission, and exact monthly strategy/diplomacy limit decay through the engine harness. |
|
||||||
| `test/npcGeneralDomesticTurn.test.ts` | corrected | contract | Fixed seed chooses the security command, applies exactly `+50`, leaves other city values unchanged, and advances exactly one tick. |
|
| `test/npcGeneralDomesticTurn.test.ts` | corrected | contract | Fixed seed chooses the security command, applies exactly `+50`, leaves other city values unchanged, and advances exactly one tick. |
|
||||||
|
|||||||
Reference in New Issue
Block a user