장수 55종과 수뇌 35종의 상태, 로그, 메시지, 예약 턴 비교를 닫습니다. 실제 시나리오 프로필과 대표 PostgreSQL 수명주기, 즉시 외교와 출병 회귀를 추가하고 발견된 Ref 로그 및 생성 장수 저장 차이를 교정합니다.
551 lines
24 KiB
TypeScript
551 lines
24 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
import { asRecord } from '@sammo-ts/common';
|
|
import { createDatabaseTurnHooks } from '@sammo-ts/game-engine/turn/databaseHooks.js';
|
|
import { InMemoryTurnWorld } from '@sammo-ts/game-engine/turn/inMemoryWorld.js';
|
|
import { createReservedTurnHandler } from '@sammo-ts/game-engine/turn/reservedTurnHandler.js';
|
|
import { InMemoryReservedTurnStore } from '@sammo-ts/game-engine/turn/reservedTurnStore.js';
|
|
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '@sammo-ts/game-engine/turn/types.js';
|
|
import { loadMapDefinitionByName } from '@sammo-ts/game-engine/scenario/mapLoader.js';
|
|
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
|
import { loadTurnWorldFromDatabase } from '@sammo-ts/game-engine/turn/worldLoader.js';
|
|
import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra';
|
|
|
|
import {
|
|
clearCoreTurnCommandPersistenceFixture,
|
|
seedCoreTurnCommandPersistenceFixture,
|
|
} from '../src/turn-differential/coreCommandPersistenceFixture.js';
|
|
import {
|
|
buildCoreTurnCommandWorldInput,
|
|
createCoreTurnCommandProfile,
|
|
runCoreTurnCommandTrace,
|
|
} from '../src/turn-differential/coreCommandTrace.js';
|
|
import { readCoreDatabaseSnapshot } from '../src/turn-differential/databaseSnapshot.js';
|
|
import {
|
|
addedFullLifecycleReferenceLogs,
|
|
fullLifecycleSnapshotSelector,
|
|
fullLifecycleTurnCommandRequest as request,
|
|
projectFullLifecycleSnapshotGraph,
|
|
} from '../src/turn-differential/fullLifecycleFixture.js';
|
|
import { runReferenceFullLifecycleTrace } from '../src/turn-differential/fullLifecycleTrace.js';
|
|
import { normalizeStoredTurnLogText, orderedSemanticLogStreams } from '../src/turn-differential/logProjection.js';
|
|
import { findTurnDifferentialWorkspaceRoot } from '../src/turn-differential/referenceSnapshot.js';
|
|
|
|
const databaseUrl = process.env.TURN_FULL_LIFECYCLE_PERSISTENCE_DATABASE_URL;
|
|
const workspaceRoot = process.env.TURN_DIFFERENTIAL_WORKSPACE_ROOT ?? findTurnDifferentialWorkspaceRoot(process.cwd());
|
|
const referenceSourceRoot = workspaceRoot
|
|
? path.resolve(process.env.REF_COMPARE_SOURCE_ROOT ?? path.join(workspaceRoot, 'ref/sam'))
|
|
: null;
|
|
const hasFullLifecycleRunner =
|
|
referenceSourceRoot !== null &&
|
|
fs.existsSync(path.join(referenceSourceRoot, 'hwe/compare/turn_full_lifecycle_trace.php'));
|
|
const databaseIntegration = describe.skipIf(!databaseUrl);
|
|
const leaseOwner = 'turn-full-lifecycle-persistence-daemon';
|
|
const dedicatedSuffix = 'turn_full_lifecycle_persistence';
|
|
|
|
export const assertDedicatedTurnFullLifecycleDatabase = (rawUrl: string): void => {
|
|
const url = new URL(rawUrl);
|
|
const schema = url.searchParams.get('schema');
|
|
const databaseName = decodeURIComponent(url.pathname.replace(/^\/+/, ''));
|
|
if (!schema?.endsWith(dedicatedSuffix) && !databaseName.endsWith(dedicatedSuffix)) {
|
|
throw new Error(
|
|
`Refusing to mutate non-dedicated turn full-lifecycle database: schema=${schema ?? '(missing)'}, database=${databaseName || '(missing)'}`
|
|
);
|
|
}
|
|
};
|
|
|
|
describe('turn full-lifecycle persistence database guard', () => {
|
|
it('rejects a shared database and schema before connecting', () => {
|
|
expect(() =>
|
|
assertDedicatedTurnFullLifecycleDatabase('postgresql://fixture:fixture@127.0.0.1:5432/sammo?schema=public')
|
|
).toThrow('Refusing to mutate non-dedicated turn full-lifecycle database');
|
|
});
|
|
|
|
it('accepts only an explicitly dedicated schema or database name', () => {
|
|
expect(() =>
|
|
assertDedicatedTurnFullLifecycleDatabase(
|
|
'postgresql://fixture:fixture@127.0.0.1:5432/sammo?schema=ci_turn_full_lifecycle_persistence'
|
|
)
|
|
).not.toThrow();
|
|
expect(() =>
|
|
assertDedicatedTurnFullLifecycleDatabase(
|
|
'postgresql://fixture:fixture@127.0.0.1:5432/ci_turn_full_lifecycle_persistence'
|
|
)
|
|
).not.toThrow();
|
|
});
|
|
});
|
|
|
|
databaseIntegration('Core PostgreSQL full reserved-turn lifecycle persistence', () => {
|
|
let db: GamePrismaClient | undefined;
|
|
let disconnect: (() => Promise<void>) | undefined;
|
|
|
|
beforeAll(async () => {
|
|
assertDedicatedTurnFullLifecycleDatabase(databaseUrl!);
|
|
const connector = createGamePostgresConnector({ url: databaseUrl! });
|
|
await connector.connect();
|
|
db = connector.prisma;
|
|
disconnect = () => connector.disconnect();
|
|
await clearCoreTurnCommandPersistenceFixture(db);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
if (db) {
|
|
await clearCoreTurnCommandPersistenceFixture(db);
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
if (db) {
|
|
await clearCoreTurnCommandPersistenceFixture(db);
|
|
}
|
|
} finally {
|
|
await disconnect?.();
|
|
}
|
|
});
|
|
|
|
it.skipIf(!workspaceRoot || !hasFullLifecycleRunner || process.env.TURN_DIFFERENTIAL_REFERENCE !== '1')(
|
|
'commits nation then general, both queue shifts, ordered logs, and reloadable state in one flush',
|
|
async () => {
|
|
if (!db) {
|
|
throw new Error('fixture database is not connected');
|
|
}
|
|
const reference = runReferenceFullLifecycleTrace(workspaceRoot!, {
|
|
...request,
|
|
generalAction: request.action,
|
|
generalArgs: request.args,
|
|
nationAction: 'che_국호변경',
|
|
nationArgs: { nationName: '수명주기국' },
|
|
} as unknown as Record<string, unknown>);
|
|
const expected = await runCoreTurnCommandTrace(request, reference.before);
|
|
const unitSet = await loadUnitSetDefinitionByName('che');
|
|
const map = await loadMapDefinitionByName('che');
|
|
const worldInput = buildCoreTurnCommandWorldInput(request, reference.before, unitSet, map);
|
|
|
|
await seedCoreTurnCommandPersistenceFixture(db, {
|
|
worldInput,
|
|
scenarioCode: 'turn-full-lifecycle-persistence',
|
|
generalTurns: reference.before.generalTurns,
|
|
nationTurns: reference.before.nationTurns,
|
|
});
|
|
const before = await readCoreDatabaseSnapshot(databaseUrl!, fullLifecycleSnapshotSelector);
|
|
expect(projectFullLifecycleSnapshotGraph(before)).toEqual(
|
|
projectFullLifecycleSnapshotGraph(reference.before)
|
|
);
|
|
expect(projectFullLifecycleSnapshotGraph(before)).toEqual(
|
|
projectFullLifecycleSnapshotGraph(expected.before)
|
|
);
|
|
|
|
const loaded = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
|
|
expect(loaded.snapshot.scenarioConfig).toEqual(worldInput.snapshot.scenarioConfig);
|
|
expect(loaded.snapshot.scenarioMeta).toEqual(worldInput.snapshot.scenarioMeta);
|
|
const reservedTurns = new InMemoryReservedTurnStore(db, {
|
|
maxGeneralTurns: 30,
|
|
maxNationTurns: 12,
|
|
leaseOwner,
|
|
leaseDurationMs: 60_000,
|
|
});
|
|
await reservedTurns.loadAll();
|
|
const loadedActor = loaded.snapshot.generals.find((general) => general.id === request.actorGeneralId);
|
|
if (!loadedActor) {
|
|
throw new Error('fixture actor is missing after database load');
|
|
}
|
|
await reservedTurns.prepareTurnsForExecution(loadedActor.id, {
|
|
nationId: loadedActor.nationId,
|
|
officerLevel: loadedActor.officerLevel,
|
|
});
|
|
|
|
const lifecycleActions: Array<{ kind: string; requestedAction: string; usedFallback: boolean }> = [];
|
|
let world: InMemoryTurnWorld | null = null;
|
|
const handler = await createReservedTurnHandler({
|
|
reservedTurns,
|
|
scenarioConfig: loaded.snapshot.scenarioConfig,
|
|
scenarioMeta: loaded.snapshot.scenarioMeta,
|
|
map: loaded.snapshot.map,
|
|
unitSet: loaded.snapshot.unitSet,
|
|
getWorld: () => world,
|
|
now: () => new Date(loaded.state.lastTurnTime),
|
|
commandProfile: createCoreTurnCommandProfile(request),
|
|
onActionResolved: (entry) => {
|
|
lifecycleActions.push({
|
|
kind: entry.kind,
|
|
requestedAction: entry.requestedAction,
|
|
usedFallback: entry.usedFallback,
|
|
});
|
|
},
|
|
});
|
|
world = new InMemoryTurnWorld(loaded.state, loaded.snapshot, {
|
|
schedule: {
|
|
entries: [
|
|
{
|
|
startMinute: 0,
|
|
tickMinutes: Math.max(1, Math.round(loaded.state.tickSeconds / 60)),
|
|
},
|
|
],
|
|
},
|
|
generalTurnHandler: handler,
|
|
});
|
|
const actor = world.getGeneralById(request.actorGeneralId);
|
|
if (!actor) {
|
|
throw new Error('fixture actor is missing from executable world');
|
|
}
|
|
world.executeGeneralTurn(actor);
|
|
|
|
expect(lifecycleActions).toEqual([
|
|
{ kind: 'nation', requestedAction: 'che_국호변경', usedFallback: false },
|
|
{ kind: 'general', requestedAction: 'che_훈련', usedFallback: false },
|
|
]);
|
|
expect(reservedTurns.getGeneralTurn(actor.id, 0).action).toBe('휴식');
|
|
expect(reservedTurns.getNationTurn(actor.nationId, actor.officerLevel, 0).action).toBe('휴식');
|
|
const dirtyBeforeFlush = world.peekDirtyState();
|
|
expect(dirtyBeforeFlush.generals.map((entry) => entry.id)).toContain(actor.id);
|
|
expect(dirtyBeforeFlush.nations.map((entry) => entry.id)).toContain(actor.nationId);
|
|
expect(dirtyBeforeFlush.logs.length).toBeGreaterThan(0);
|
|
|
|
const databaseBeforeFlush = await readCoreDatabaseSnapshot(databaseUrl!, fullLifecycleSnapshotSelector);
|
|
expect(projectFullLifecycleSnapshotGraph(databaseBeforeFlush)).toEqual(
|
|
projectFullLifecycleSnapshotGraph(before)
|
|
);
|
|
|
|
const hooks = await createDatabaseTurnHooks(databaseUrl!, world, { reservedTurns });
|
|
try {
|
|
if (!hooks.hooks.flushChanges) {
|
|
throw new Error('database turn hooks do not expose flushChanges');
|
|
}
|
|
await hooks.hooks.flushChanges({
|
|
lastTurnTime: loaded.state.lastTurnTime.toISOString(),
|
|
processedGenerals: 1,
|
|
processedTurns: 1,
|
|
durationMs: 0,
|
|
partial: false,
|
|
});
|
|
} finally {
|
|
await hooks.close();
|
|
}
|
|
|
|
expect(world.peekDirtyState().logs).toEqual([]);
|
|
expect(reservedTurns.peekDirtyState()).toMatchObject({
|
|
generalIds: [],
|
|
nationKeys: [],
|
|
});
|
|
|
|
const after = await readCoreDatabaseSnapshot(databaseUrl!, fullLifecycleSnapshotSelector);
|
|
expect(projectFullLifecycleSnapshotGraph(after)).toEqual(
|
|
projectFullLifecycleSnapshotGraph(reference.after)
|
|
);
|
|
expect(projectFullLifecycleSnapshotGraph(after)).toEqual(projectFullLifecycleSnapshotGraph(expected.after));
|
|
expect(orderedSemanticLogStreams(after.logs)).toEqual(
|
|
orderedSemanticLogStreams(addedFullLifecycleReferenceLogs(reference.before, reference.after))
|
|
);
|
|
expect(orderedSemanticLogStreams(after.logs)).toEqual(orderedSemanticLogStreams(expected.after.logs));
|
|
const persistedActionTexts = after.logs
|
|
.filter((entry) => String(entry.category).toLowerCase() === 'action')
|
|
.map((entry) => normalizeStoredTurnLogText(entry.text));
|
|
expect(persistedActionTexts.findIndex((text) => text.includes('국호를'))).toBeLessThan(
|
|
persistedActionTexts.findIndex((text) => text.includes('훈련치가'))
|
|
);
|
|
|
|
const reloaded = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
|
|
const reloadedActor = reloaded.snapshot.generals.find((general) => general.id === request.actorGeneralId);
|
|
const expectedActor = expected.after.generals.find((general) => general.id === request.actorGeneralId);
|
|
expect(reloadedActor).toMatchObject({
|
|
train: expectedActor?.train,
|
|
atmos: expectedActor?.atmos,
|
|
experience: expectedActor?.experience,
|
|
dedication: expectedActor?.dedication,
|
|
});
|
|
expect(asRecord(reloadedActor?.meta)).toMatchObject({
|
|
killturn: expectedActor?.killTurn,
|
|
myset: expectedActor?.mySet,
|
|
});
|
|
expect(reloaded.snapshot.nations.find((nation) => nation.id === actor.nationId)?.name).toBe('수명주기국');
|
|
|
|
const reloadedReservedTurns = new InMemoryReservedTurnStore(db, {
|
|
maxGeneralTurns: 30,
|
|
maxNationTurns: 12,
|
|
});
|
|
await reloadedReservedTurns.loadAll();
|
|
expect(reloadedReservedTurns.getGeneralTurn(actor.id, 0).action).toBe('휴식');
|
|
expect(reloadedReservedTurns.getNationTurn(actor.nationId, actor.officerLevel, 0).action).toBe('휴식');
|
|
},
|
|
180_000
|
|
);
|
|
|
|
it('persists thirty resting turns for every command-created volunteer and reloads them', async () => {
|
|
if (!db) {
|
|
throw new Error('fixture database is not connected');
|
|
}
|
|
const map = await loadMapDefinitionByName('che');
|
|
const unitSet = await loadUnitSetDefinitionByName('che');
|
|
const cityDefinition = map.cities.find((city) => city.id === 3) ?? map.cities[0];
|
|
if (!cityDefinition) {
|
|
throw new Error('fixture map has no city');
|
|
}
|
|
const actorId = 101;
|
|
const nationId = 11;
|
|
const actionTime = new Date('0190-01-01T00:00:00.000Z');
|
|
const actor: TurnGeneral = {
|
|
id: actorId,
|
|
userId: null,
|
|
name: '영속화의병장',
|
|
nationId,
|
|
cityId: cityDefinition.id,
|
|
troopId: 0,
|
|
stats: { leadership: 90, strength: 80, intelligence: 70 },
|
|
experience: 1_000,
|
|
dedication: 1_000,
|
|
officerLevel: 12,
|
|
role: {
|
|
personality: null,
|
|
specialDomestic: null,
|
|
specialWar: null,
|
|
items: { horse: null, weapon: null, book: null, item: null },
|
|
},
|
|
injury: 0,
|
|
gold: 100_000,
|
|
rice: 100_000,
|
|
crew: 1_000,
|
|
crewTypeId: 1_100,
|
|
train: 50,
|
|
atmos: 50,
|
|
age: 30,
|
|
npcState: 0,
|
|
bornYear: 160,
|
|
deadYear: 260,
|
|
affinity: 50,
|
|
picture: 'default.jpg',
|
|
imageServer: 0,
|
|
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
|
meta: {
|
|
killturn: 24,
|
|
officer_city: cityDefinition.id,
|
|
belong: 10,
|
|
permission: 'normal',
|
|
},
|
|
turnTime: actionTime,
|
|
};
|
|
const state: TurnWorldState = {
|
|
id: 1,
|
|
currentYear: 190,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
lastTurnTime: actionTime,
|
|
clockBaseTime: actionTime,
|
|
clockTick: 0,
|
|
clockMode: 'manual',
|
|
clockWallAnchor: actionTime,
|
|
lastTurnTick: 0,
|
|
meta: {
|
|
hiddenSeed: 'turn-command-volunteer-persistence',
|
|
killturn: 24,
|
|
lastTurnTime: actionTime.toISOString(),
|
|
},
|
|
};
|
|
const snapshot: TurnWorldSnapshot = {
|
|
scenarioConfig: {
|
|
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 75, npcMin: 10, chiefMin: 70 },
|
|
iconPath: '',
|
|
map: {},
|
|
const: {
|
|
develCost: 100,
|
|
openingPartYear: 3,
|
|
defaultMaxGeneral: 500,
|
|
initialNationGenLimit: 10,
|
|
defaultNpcGold: 1_000,
|
|
defaultNpcRice: 1_000,
|
|
defaultCrewTypeId: 1_100,
|
|
retirementYear: 80,
|
|
randGenFirstName: ['가'],
|
|
randGenMiddleName: [''],
|
|
randGenLastName: ['가'],
|
|
availablePersonality: ['che_안전'],
|
|
},
|
|
environment: { mapName: map.id, unitSet: unitSet.id },
|
|
},
|
|
scenarioMeta: {
|
|
title: '명령 생성 장수 예약 턴 영속화',
|
|
startYear: 180,
|
|
life: null,
|
|
fiction: 0,
|
|
history: [],
|
|
ignoreDefaultEvents: false,
|
|
},
|
|
map,
|
|
unitSet,
|
|
nations: [
|
|
{
|
|
id: nationId,
|
|
name: '의병국',
|
|
color: '#777777',
|
|
capitalCityId: cityDefinition.id,
|
|
chiefGeneralId: actorId,
|
|
gold: 1_000_000,
|
|
rice: 1_000_000,
|
|
power: 0,
|
|
level: 1,
|
|
typeCode: 'che_명가',
|
|
meta: {
|
|
gennum: 1,
|
|
tech: 1_000,
|
|
strategic_cmd_limit: 0,
|
|
turn_last_12: { command: '의병모집', arg: {}, term: 2 },
|
|
},
|
|
},
|
|
],
|
|
cities: [
|
|
{
|
|
id: cityDefinition.id,
|
|
name: cityDefinition.name,
|
|
nationId,
|
|
level: cityDefinition.level,
|
|
state: 0,
|
|
population: cityDefinition.initial.population,
|
|
populationMax: cityDefinition.max.population,
|
|
agriculture: cityDefinition.initial.agriculture,
|
|
agricultureMax: cityDefinition.max.agriculture,
|
|
commerce: cityDefinition.initial.commerce,
|
|
commerceMax: cityDefinition.max.commerce,
|
|
security: cityDefinition.initial.security,
|
|
securityMax: cityDefinition.max.security,
|
|
supplyState: map.defaults?.supplyState ?? 1,
|
|
frontState: map.defaults?.frontState ?? 0,
|
|
defence: cityDefinition.initial.defence,
|
|
defenceMax: cityDefinition.max.defence,
|
|
wall: cityDefinition.initial.wall,
|
|
wallMax: cityDefinition.max.wall,
|
|
conflict: {},
|
|
meta: {
|
|
trust: map.defaults?.trust ?? 50,
|
|
trade: map.defaults?.trade ?? 100,
|
|
region: cityDefinition.region,
|
|
},
|
|
},
|
|
],
|
|
generals: [actor],
|
|
troops: [],
|
|
diplomacy: [],
|
|
events: [],
|
|
initialEvents: [],
|
|
};
|
|
await seedCoreTurnCommandPersistenceFixture(db, {
|
|
worldInput: { state, snapshot, map },
|
|
scenarioCode: 'turn-command-volunteer-persistence',
|
|
generalTurns: Array.from({ length: 30 }, (_, turnIndex) => ({
|
|
generalId: actorId,
|
|
turnIndex,
|
|
action: '휴식',
|
|
args: {},
|
|
})),
|
|
nationTurns: Array.from({ length: 12 }, (_, turnIndex) => ({
|
|
nationId,
|
|
officerLevel: 12,
|
|
turnIndex,
|
|
action: turnIndex === 0 ? 'che_의병모집' : '휴식',
|
|
args: {},
|
|
})),
|
|
});
|
|
|
|
const loaded = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
|
|
const reservedTurns = new InMemoryReservedTurnStore(db, {
|
|
maxGeneralTurns: 30,
|
|
maxNationTurns: 12,
|
|
leaseOwner: `${leaseOwner}-volunteer`,
|
|
leaseDurationMs: 60_000,
|
|
});
|
|
await reservedTurns.loadAll();
|
|
const loadedActor = loaded.snapshot.generals.find((general) => general.id === actorId);
|
|
if (!loadedActor) {
|
|
throw new Error('volunteer fixture actor is missing after database load');
|
|
}
|
|
await reservedTurns.prepareTurnsForExecution(actorId, { nationId, officerLevel: 12 });
|
|
|
|
let world: InMemoryTurnWorld | null = null;
|
|
const handler = await createReservedTurnHandler({
|
|
reservedTurns,
|
|
scenarioConfig: loaded.snapshot.scenarioConfig,
|
|
scenarioMeta: loaded.snapshot.scenarioMeta,
|
|
map: loaded.snapshot.map,
|
|
unitSet: loaded.snapshot.unitSet,
|
|
getWorld: () => world,
|
|
now: () => new Date(loaded.state.lastTurnTime),
|
|
commandProfile: {
|
|
general: ['휴식'],
|
|
nation: ['che_의병모집', '휴식'],
|
|
},
|
|
});
|
|
world = new InMemoryTurnWorld(loaded.state, loaded.snapshot, {
|
|
schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] },
|
|
generalTurnHandler: handler,
|
|
});
|
|
const executableActor = world.getGeneralById(actorId);
|
|
if (!executableActor) {
|
|
throw new Error('volunteer fixture actor is missing from executable world');
|
|
}
|
|
world.executeGeneralTurn(executableActor);
|
|
|
|
const createdIds = world
|
|
.peekDirtyState()
|
|
.createdGenerals.map((general) => general.id)
|
|
.sort((left, right) => left - right);
|
|
expect(createdIds).toEqual([102, 103, 104]);
|
|
expect(reservedTurns.peekDirtyState().generalInitializationIds.sort((left, right) => left - right)).toEqual(
|
|
createdIds
|
|
);
|
|
const restingTurns = Array.from({ length: 30 }, () => ({ action: '휴식', args: {} }));
|
|
for (const generalId of createdIds) {
|
|
expect(reservedTurns.getGeneralTurns(generalId)).toEqual(restingTurns);
|
|
}
|
|
|
|
const hooks = await createDatabaseTurnHooks(databaseUrl!, world, { reservedTurns });
|
|
try {
|
|
if (!hooks.hooks.flushChanges) {
|
|
throw new Error('database turn hooks do not expose flushChanges');
|
|
}
|
|
await hooks.hooks.flushChanges({
|
|
lastTurnTime: loaded.state.lastTurnTime.toISOString(),
|
|
processedGenerals: 1,
|
|
processedTurns: 1,
|
|
durationMs: 0,
|
|
partial: false,
|
|
});
|
|
} finally {
|
|
await hooks.close();
|
|
}
|
|
|
|
const persistedTurns = await db.generalTurn.findMany({
|
|
where: { generalId: { in: createdIds } },
|
|
orderBy: [{ generalId: 'asc' }, { turnIdx: 'asc' }],
|
|
});
|
|
expect(persistedTurns).toHaveLength(createdIds.length * 30);
|
|
for (const generalId of createdIds) {
|
|
expect(
|
|
persistedTurns
|
|
.filter((turn) => turn.generalId === generalId)
|
|
.map((turn) => ({ turnIdx: turn.turnIdx, action: turn.actionCode, args: turn.arg }))
|
|
).toEqual(Array.from({ length: 30 }, (_, turnIdx) => ({ turnIdx, action: '휴식', args: {} })));
|
|
}
|
|
|
|
const persistedNation = await db.nation.findUnique({ where: { id: nationId }, select: { meta: true } });
|
|
expect(persistedNation?.meta).toMatchObject({ gennum: 4 });
|
|
|
|
const reloaded = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
|
|
expect(
|
|
reloaded.snapshot.generals
|
|
.filter((general) => createdIds.includes(general.id))
|
|
.map((general) => general.id)
|
|
.sort((left, right) => left - right)
|
|
).toEqual(createdIds);
|
|
expect(reloaded.snapshot.nations.find((nation) => nation.id === nationId)?.meta).toMatchObject({ gennum: 4 });
|
|
const reloadedReservedTurns = new InMemoryReservedTurnStore(db, {
|
|
maxGeneralTurns: 30,
|
|
maxNationTurns: 12,
|
|
});
|
|
await reloadedReservedTurns.loadAll();
|
|
for (const generalId of createdIds) {
|
|
expect(reloadedReservedTurns.getGeneralTurns(generalId)).toEqual(restingTurns);
|
|
}
|
|
}, 180_000);
|
|
});
|