플레이 감사 국가 생멸과 장기 tick 저장을 보완하고 중간 전달 준비
This commit is contained in:
@@ -49,6 +49,7 @@ integration('monthly diplomacy persistence', () => {
|
||||
db = connector.prisma;
|
||||
closeDb = () => connector.disconnect();
|
||||
await db.playAuditDiplomacyEvent.deleteMany({ where: { serverId: scenarioCode } });
|
||||
await db.playAuditPolicy.deleteMany({ where: { serverId: scenarioCode } });
|
||||
await db.diplomacy.deleteMany({
|
||||
where: {
|
||||
OR: [{ srcNationId: { in: nationIds } }, { destNationId: { in: nationIds } }],
|
||||
@@ -61,6 +62,7 @@ integration('monthly diplomacy persistence', () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await db.playAuditDiplomacyEvent.deleteMany({ where: { serverId: scenarioCode } });
|
||||
await db.playAuditPolicy.deleteMany({ where: { serverId: scenarioCode } });
|
||||
await db.diplomacy.deleteMany({
|
||||
where: {
|
||||
OR: [{ srcNationId: { in: nationIds } }, { destNationId: { in: nationIds } }],
|
||||
@@ -278,6 +280,54 @@ integration('monthly diplomacy persistence', () => {
|
||||
currentYear: 193,
|
||||
currentMonth: 4,
|
||||
});
|
||||
const endingNation = nationIds[3]!;
|
||||
expect(world.removeNation(endingNation)).toBe(true);
|
||||
const removals = world.peekDirtyState().pendingAuditDiplomacy;
|
||||
expect(removals).toHaveLength(6);
|
||||
expect(
|
||||
removals.every((event) => event.eventType === 'NATION_RELATION_REMOVED' && event.after === null)
|
||||
).toBe(true);
|
||||
await db.$executeRawUnsafe(`CREATE FUNCTION reject_lifecycle_audit_fixture() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN RAISE EXCEPTION 'lifecycle audit fixture failure'; END; $$`);
|
||||
await db.$executeRawUnsafe(`CREATE TRIGGER reject_lifecycle_audit_fixture BEFORE INSERT ON play_audit_diplomacy_event
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_lifecycle_audit_fixture()`);
|
||||
try {
|
||||
await expect(hooks.flushChanges()).rejects.toThrow('lifecycle audit fixture failure');
|
||||
expect(await db.nation.count({ where: { id: endingNation } })).toBe(1);
|
||||
expect(world.peekDirtyState().pendingAuditDiplomacy).toEqual(removals);
|
||||
} finally {
|
||||
await db.$executeRawUnsafe('DROP TRIGGER reject_lifecycle_audit_fixture ON play_audit_diplomacy_event');
|
||||
await db.$executeRawUnsafe('DROP FUNCTION reject_lifecycle_audit_fixture()');
|
||||
}
|
||||
await hooks.flushChanges();
|
||||
expect(await db.nation.count({ where: { id: endingNation } })).toBe(0);
|
||||
expect(
|
||||
await db.diplomacy.count({
|
||||
where: { OR: [{ srcNationId: endingNation }, { destNationId: endingNation }] },
|
||||
})
|
||||
).toBe(0);
|
||||
expect(
|
||||
await db.playAuditDiplomacyEvent.count({
|
||||
where: { serverId: scenarioCode, eventType: 'NATION_RELATION_REMOVED' },
|
||||
})
|
||||
).toBe(6);
|
||||
expect(world.addNation(buildNation(endingNation, '재건국', 1))).toBe(true);
|
||||
await hooks.flushChanges();
|
||||
expect(await db.nation.count({ where: { id: endingNation } })).toBe(1);
|
||||
expect(
|
||||
await db.diplomacy.count({
|
||||
where: { OR: [{ srcNationId: endingNation }, { destNationId: endingNation }] },
|
||||
})
|
||||
).toBe(6);
|
||||
expect(await db.playAuditPolicy.count({ where: { serverId: scenarioCode, nationId: endingNation } })).toBe(
|
||||
4
|
||||
);
|
||||
await hooks.flushChanges();
|
||||
expect(
|
||||
await db.playAuditDiplomacyEvent.count({
|
||||
where: { serverId: scenarioCode, eventType: 'NATION_RELATION_CREATED' },
|
||||
})
|
||||
).toBe(6);
|
||||
} finally {
|
||||
await hooks.close();
|
||||
}
|
||||
|
||||
@@ -129,10 +129,67 @@ const buildWorld = (generalTurnHandler?: GeneralTurnHandler) => {
|
||||
return world;
|
||||
};
|
||||
describe('play audit collection durability state', () => {
|
||||
it('captures direct reserved-turn nation batches once and initializes their policies', () => {
|
||||
const world = buildWorld({
|
||||
execute: () => ({ created: { generals: [], nations: [buildNation(3, 0, {}), buildNation(4, 0, {})] } }),
|
||||
});
|
||||
world.updateGeneral(3, { turnTick: 123 });
|
||||
world.executeGeneralTurn(world.getGeneralById(3)!);
|
||||
const changes = world.peekDirtyState();
|
||||
expect(changes.pendingAuditDiplomacy).toHaveLength(10);
|
||||
expect(changes.pendingAuditDiplomacy.every((event) => event.tick === 123n)).toBe(true);
|
||||
expect(
|
||||
new Set(changes.pendingAuditDiplomacy.map((event) => `${event.srcNationId}:${event.destNationId}`)).size
|
||||
).toBe(10);
|
||||
expect(changes.pendingAuditPolicies).toHaveLength(8);
|
||||
expect(world.getNationById(3)?.meta._playAuditPolicy).toBeDefined();
|
||||
expect(world.getNationById(4)?.meta._playAuditPolicy).toBeDefined();
|
||||
});
|
||||
|
||||
it('keeps nation creation and removal relations ordered across repeated IDs and rollback', () => {
|
||||
const world = buildWorld();
|
||||
const checkpoint = world.captureState();
|
||||
const nation = buildNation(3, 0, {});
|
||||
expect(world.addNation(nation)).toBe(true);
|
||||
const created = world.peekDirtyState().pendingAuditDiplomacy;
|
||||
expect(created).toHaveLength(4);
|
||||
expect(created.map(({ srcNationId, destNationId }) => [srcNationId, destNationId])).toEqual([
|
||||
[1, 3],
|
||||
[2, 3],
|
||||
[3, 1],
|
||||
[3, 2],
|
||||
]);
|
||||
expect(created.every((event) => event.before === null && event.eventType === 'NATION_RELATION_CREATED')).toBe(
|
||||
true
|
||||
);
|
||||
expect(world.addNation(nation)).toBe(false);
|
||||
expect(world.peekDirtyState().pendingAuditDiplomacy).toEqual(created);
|
||||
world.applyDiplomacyPatch({ srcNationId: 1, destNationId: 3, patch: { state: 7, term: 12 } });
|
||||
expect(world.removeNation(3)).toBe(true);
|
||||
const removed = world.peekDirtyState().pendingAuditDiplomacy.slice(4);
|
||||
expect(removed).toHaveLength(4);
|
||||
expect(removed[0]).toMatchObject({
|
||||
eventType: 'NATION_RELATION_REMOVED',
|
||||
after: null,
|
||||
before: { state: 7, term: 12, dead: 0 },
|
||||
});
|
||||
expect(world.removeNation(3)).toBe(false);
|
||||
expect(world.addNation(nation)).toBe(true);
|
||||
const replayedId = world.peekDirtyState().pendingAuditDiplomacy.slice(8);
|
||||
expect(replayedId).toHaveLength(4);
|
||||
expect(replayedId[0]!.executionId).not.toBe(created[0]!.executionId);
|
||||
world.restoreState(checkpoint);
|
||||
expect(world.peekDirtyState().pendingAuditDiplomacy).toEqual([]);
|
||||
expect(world.addNation(nation)).toBe(true);
|
||||
expect(world.peekDirtyState().pendingAuditDiplomacy).toEqual(created);
|
||||
});
|
||||
|
||||
it('marks an empty diplomacy baseline without inventing relations and validates before queuing', () => {
|
||||
const world = buildWorld();
|
||||
world.updateWorldMeta({ serverId: null });
|
||||
world.removeNation(1);
|
||||
world.removeNation(2);
|
||||
world.updateWorldMeta({ serverId: 'yearbook-projection-test' });
|
||||
expect(() => initializeAuditDiplomacy(world, new Date('invalid'))).toThrow(RangeError);
|
||||
expect(world.getState().meta.playAuditDiplomacy).toBeUndefined();
|
||||
expect(world.peekDirtyState().pendingAuditDiplomacy).toEqual([]);
|
||||
|
||||
@@ -14,7 +14,7 @@ integration('play audit transactional month persistence', () => {
|
||||
serverId,
|
||||
year: 200,
|
||||
month: 1,
|
||||
tick: 10,
|
||||
tick: 4_320_000_000,
|
||||
kind: 'MONTH_END',
|
||||
settlementsComplete: true,
|
||||
...buildAuditSnapshot({
|
||||
@@ -67,7 +67,7 @@ integration('play audit transactional month persistence', () => {
|
||||
await expect(db.$transaction((tx) => persistAuditMonth(tx, { ...snapshot, tick: 11 }))).rejects.toThrow(
|
||||
'replay payload conflict'
|
||||
);
|
||||
expect((await db.playAuditMonth.findUniqueOrThrow({ where: { id: saved.id } })).tick).toBe(10);
|
||||
expect((await db.playAuditMonth.findUniqueOrThrow({ where: { id: saved.id } })).tick).toBe(4_320_000_000n);
|
||||
await db.playAuditMonth.delete({ where: { id: saved.id } });
|
||||
expect(await db.playAuditNation.count({ where: { sampleId: saved.id } })).toBe(0);
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ integration('immutable policy persistence', () => {
|
||||
source: 'BASELINE',
|
||||
year: 190,
|
||||
month: 1,
|
||||
tick: 1,
|
||||
tick: 4_320_000_000,
|
||||
requestId: null,
|
||||
ordinal: 1,
|
||||
actor: null,
|
||||
@@ -91,7 +91,7 @@ integration('immutable policy persistence', () => {
|
||||
await db.$transaction((tx) => persistAuditPolicies(tx, [baseline, change], context));
|
||||
const rows = await db.playAuditPolicy.findMany({ where: { serverId }, orderBy: { revision: 'asc' } });
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]).toMatchObject({ inputSequence: null, actor: null });
|
||||
expect(rows[0]).toMatchObject({ inputSequence: null, actor: null, tick: 4_320_000_000n });
|
||||
expect(rows[1]).toMatchObject({
|
||||
inputSequence: input.sequence,
|
||||
requestId,
|
||||
|
||||
@@ -193,7 +193,7 @@ integration('initial audit durability before runtime readiness', () => {
|
||||
policies.every(
|
||||
(policy) =>
|
||||
policy.source === 'BASELINE' &&
|
||||
policy.tick === Number(beforeClock.clockTick) &&
|
||||
policy.tick === beforeClock.clockTick &&
|
||||
policy.inputSequence === null
|
||||
)
|
||||
).toBe(true);
|
||||
@@ -256,7 +256,7 @@ integration('initial audit durability before runtime readiness', () => {
|
||||
expect(recovered.clockRevision).toBeGreaterThan(original.clockRevision);
|
||||
const policies = await db.playAuditPolicy.findMany({ where: { serverId, nationId: 91992 } });
|
||||
expect(policies).toHaveLength(4);
|
||||
expect(policies.every((policy) => policy.tick === runtime!.world.getGameClockState().tick)).toBe(true);
|
||||
expect(policies.every((policy) => policy.tick === BigInt(runtime!.world.getGameClockState().tick))).toBe(true);
|
||||
expect(await db.playAuditMonth.findMany({ where: { serverId, kind: 'INITIAL' } })).toEqual([initial]);
|
||||
}, 30_000);
|
||||
it('adopts an empty document collection without duplicating an existing initial sample', async () => {
|
||||
|
||||
Reference in New Issue
Block a user