test: add turn command state differential harness

This commit is contained in:
2026-07-25 11:38:42 +00:00
parent 0151055f94
commit 111d3c7074
16 changed files with 1200 additions and 31 deletions
@@ -0,0 +1,235 @@
export type CanonicalEngine = 'ref' | 'core2026';
export interface TurnSnapshotSelector {
generalIds: number[];
cityIds: number[];
nationIds: number[];
logAfterId?: number;
messageAfterId?: number;
}
export interface CanonicalTurnSnapshot {
schemaVersion: 1;
engine: CanonicalEngine;
world: Record<string, unknown>;
generals: Array<Record<string, unknown>>;
cities: Array<Record<string, unknown>>;
nations: Array<Record<string, unknown>>;
diplomacy: Array<Record<string, unknown>>;
generalTurns: Array<Record<string, unknown>>;
nationTurns: Array<Record<string, unknown>>;
logs: Array<Record<string, unknown>>;
messages: Array<Record<string, unknown>>;
watermarks: {
logId: number;
messageId: number;
};
}
export interface CanonicalTurnCommandTrace {
schemaVersion: 1;
engine: CanonicalEngine;
execution: {
kind: 'general' | 'nation';
actorGeneralId: number;
action: string;
args: unknown;
seedDomain: 'generalCommand' | 'nationCommand';
outcome?: unknown;
};
before: CanonicalTurnSnapshot;
after: CanonicalTurnSnapshot;
rng: Array<{
seq: number;
operation: string;
arguments: Record<string, unknown>;
result: unknown;
}>;
}
const legacyArgumentAliases: Readonly<Record<string, string>> = {
destCityID: 'destCityId',
destNationID: 'destNationId',
destGeneralID: 'destGeneralId',
destTroopID: 'destTroopId',
};
export const canonicalizeTurnCommandArgs = (value: unknown): unknown => {
if (Array.isArray(value)) {
return value.map(canonicalizeTurnCommandArgs);
}
if (typeof value !== 'object' || value === null) {
return value;
}
return Object.fromEntries(
Object.entries(value as Record<string, unknown>)
.map(([key, entry]) => [legacyArgumentAliases[key] ?? key, canonicalizeTurnCommandArgs(entry)] as const)
.sort(([left], [right]) => left.localeCompare(right))
);
};
const asRecord = (value: unknown): Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
const readNumber = (record: Record<string, unknown>, key: string, fallback = 0): number => {
const value = record[key];
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
};
const readString = (record: Record<string, unknown>, key: string): string | null => {
const value = record[key];
return typeof value === 'string' ? value : null;
};
const serializeDate = (value: Date | null): string | null => value?.toISOString() ?? null;
export const projectCoreDatabaseSnapshot = (rows: {
world: {
currentYear: number;
currentMonth: number;
tickSeconds: number;
meta: unknown;
};
generals: Array<Record<string, unknown>>;
cities: Array<Record<string, unknown>>;
nations: Array<Record<string, unknown>>;
diplomacy: Array<Record<string, unknown>>;
generalTurns: Array<Record<string, unknown>>;
nationTurns: Array<Record<string, unknown>>;
logs: Array<Record<string, unknown>>;
}): CanonicalTurnSnapshot => {
const worldMeta = asRecord(rows.world.meta);
const generals = rows.generals.map((row) => {
const meta = asRecord(row.meta);
return {
id: row.id,
name: row.name,
nationId: row.nationId,
cityId: row.cityId,
troopId: row.troopId,
leadership: row.leadership,
strength: row.strength,
intelligence: row.intel,
experience: row.experience,
dedication: row.dedication,
officerLevel: row.officerLevel,
injury: row.injury,
gold: row.gold,
rice: row.rice,
crew: row.crew,
crewTypeId: row.crewTypeId,
train: row.train,
atmos: row.atmos,
age: row.age,
npcState: row.npcState,
turnTime: row.turnTime instanceof Date ? serializeDate(row.turnTime) : row.turnTime,
recentWarTime: row.recentWarTime instanceof Date ? serializeDate(row.recentWarTime) : row.recentWarTime,
lastTurn: row.lastTurn,
meta,
leadershipExp: readNumber(meta, 'leadership_exp'),
strengthExp: readNumber(meta, 'strength_exp'),
intelExp: readNumber(meta, 'intel_exp'),
killTurn: readNumber(meta, 'killturn'),
mySet: readNumber(meta, 'myset'),
};
});
const cities = rows.cities.map((row) => {
const meta = asRecord(row.meta);
return {
id: row.id,
name: row.name,
nationId: row.nationId,
level: row.level,
population: row.population,
populationMax: row.populationMax,
agriculture: row.agriculture,
agricultureMax: row.agricultureMax,
commerce: row.commerce,
commerceMax: row.commerceMax,
security: row.security,
securityMax: row.securityMax,
supplyState: row.supplyState,
frontState: row.frontState,
defence: row.defence,
defenceMax: row.defenceMax,
wall: row.wall,
wallMax: row.wallMax,
state: readNumber(meta, 'state'),
term: readNumber(meta, 'term'),
trust: row.trust,
trade: row.trade,
};
});
const nations = rows.nations.map((row) => {
const meta = asRecord(row.meta);
return {
id: row.id,
name: row.name,
color: row.color,
capitalCityId: row.capitalCityId,
gold: row.gold,
rice: row.rice,
tech: row.tech,
level: row.level,
typeCode: row.typeCode,
generalCount: readNumber(meta, 'gennum'),
power: readNumber(meta, 'power'),
war: readNumber(meta, 'war'),
meta,
};
});
const diplomacy = rows.diplomacy.map((row) => ({
fromNationId: row.srcNationId,
toNationId: row.destNationId,
state: row.stateCode,
term: row.term,
dead: row.isDead === true ? 1 : 0,
}));
const generalTurns = rows.generalTurns.map((row) => ({
generalId: row.generalId,
turnIndex: row.turnIdx,
action: row.actionCode,
args: row.arg,
}));
const nationTurns = rows.nationTurns.map((row) => ({
nationId: row.nationId,
officerLevel: row.officerLevel,
turnIndex: row.turnIdx,
action: row.actionCode,
args: row.arg,
}));
const logs = rows.logs.map((row) => ({
id: row.id,
scope: readString(row, 'scope'),
category: readString(row, 'category')?.toLowerCase() ?? null,
generalId: row.generalId,
nationId: row.nationId,
year: row.year,
month: row.month,
text: row.text,
}));
return {
schemaVersion: 1,
engine: 'core2026',
world: {
year: rows.world.currentYear,
month: rows.world.currentMonth,
tickMinutes: Math.max(1, Math.round(rows.world.tickSeconds / 60)),
turnTime: readString(worldMeta, 'lastTurnTime'),
isUnited: readNumber(worldMeta, 'isUnited', readNumber(worldMeta, 'isunited')),
},
generals,
cities,
nations,
diplomacy,
generalTurns,
nationTurns,
logs,
messages: [],
watermarks: {
logId: logs.reduce((max, row) => Math.max(max, Number(row.id) || 0), 0),
messageId: 0,
},
};
};