import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { loadActionModuleBundle, type Nation } from '@sammo-ts/logic'; import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra'; import { createDatabaseTurnHooks } from '../src/turn/databaseHooks.js'; import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js'; import { loadTurnWorldFromDatabase } from '../src/turn/worldLoader.js'; import { createMonthlyEventHandler } from '../src/turn/monthlyEventHandler.js'; import { createLostUniqueItemHandler, createMergeInheritPointRankHandler, } from '../src/turn/monthlyUniqueInheritAction.js'; import type { TurnEvent, TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js'; const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL; const integration = describe.skipIf(!databaseUrl); const nationId = 990_101; const generalId = 990_101; const event: TurnEvent = { id: 1, targetCode: 'month', priority: 9_000, condition: true, action: [['LostUniqueItem', 1], ['MergeInheritPointRank']], meta: {}, }; const nation: Nation = { id: nationId, name: '망실검증국', color: '#777777', capitalCityId: null, chiefGeneralId: null, gold: 0, rice: 0, power: 0, level: 2, typeCode: 'che_중립', meta: {}, }; const general: TurnGeneral = { id: generalId, userId: 'monthly-unique-user', name: '망실대상', nationId, cityId: 0, troopId: 0, stats: { leadership: 50, strength: 50, intelligence: 50 }, experience: 0, dedication: 0, officerLevel: 1, role: { personality: null, specialDomestic: null, specialWar: null, items: { horse: 'che_명마_12_옥란백용구', weapon: 'che_무기_01_단도', book: null, item: 'che_저지_삼황내문', }, }, injury: 0, gold: 0, rice: 0, crew: 0, crewTypeId: 0, train: 0, atmos: 0, age: 30, startAge: 20, npcState: 0, triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, meta: { killturn: 1, belong: 3, inherit_lived_month: 2, max_domestic_critical: 4, inherit_active_action: 5, dex1: 1000, dex2: 0, dex3: 0, dex4: 0, dex5: 0, rank_warnum: 2, firenum: 1, betwin: 2, betgold: 4000, betwingold: 2000, inherit_earned_act: 11, inherit_spent_dyn: 9, }, inheritancePoints: { previous: 999, unifier: 6, tournament: 7, }, lastTurn: { command: '휴식' }, turnTime: new Date('2026-07-25T00:00:00.000Z'), }; integration('monthly unique-item and inheritance-rank persistence', () => { let db: GamePrismaClient; let closeDb: (() => Promise) | undefined; beforeAll(async () => { const connector = createGamePostgresConnector({ url: databaseUrl! }); await connector.connect(); db = connector.prisma; closeDb = () => connector.disconnect(); await db.logEntry.deleteMany({ where: { generalId } }); await db.rankData.deleteMany({ where: { generalId } }); await db.inheritancePoint.deleteMany({ where: { userId: general.userId! } }); await db.general.deleteMany({ where: { id: generalId } }); await db.nation.deleteMany({ where: { id: nationId } }); }); afterAll(async () => { await db.logEntry.deleteMany({ where: { generalId } }); await db.rankData.deleteMany({ where: { generalId } }); await db.inheritancePoint.deleteMany({ where: { userId: general.userId! } }); await db.general.deleteMany({ where: { id: generalId } }); await db.nation.deleteMany({ where: { id: nationId } }); await closeDb?.(); }); it('commits removed item slots, item inventory, logs, and all merged rank columns', async () => { await db.nation.create({ data: { id: nation.id, name: nation.name, color: nation.color, level: nation.level, typeCode: nation.typeCode, meta: {}, }, }); await db.general.create({ data: { id: general.id, userId: general.userId, name: general.name, nationId: general.nationId, cityId: general.cityId, leadership: general.stats.leadership, strength: general.stats.strength, intel: general.stats.intelligence, age: general.age, startAge: general.startAge, npcState: general.npcState, horseCode: general.role.items.horse ?? 'None', weaponCode: general.role.items.weapon ?? 'None', itemCode: general.role.items.item ?? 'None', turnTime: general.turnTime, meta: general.meta, }, }); await db.rankData.createMany({ data: [ { generalId, nationId, type: 'warnum', value: 2 }, { generalId, nationId, type: 'firenum', value: 1 }, { generalId, nationId, type: 'betwin', value: 2 }, { generalId, nationId, type: 'betgold', value: 4000 }, { generalId, nationId, type: 'betwingold', value: 2000 }, { generalId, nationId, type: 'inherit_earned', value: 999 }, { generalId, nationId, type: 'inherit_earned_act', value: 11 }, { generalId, nationId, type: 'inherit_earned_dyn', value: 999 }, { generalId, nationId, type: 'inherit_spent', value: 999 }, { generalId, nationId, type: 'inherit_spent_dyn', value: 9 }, ], }); await db.inheritancePoint.createMany({ data: Object.entries(general.inheritancePoints!).map(([key, value]) => ({ userId: general.userId!, key, value, })), }); const worldRow = await db.worldState.create({ data: { scenarioCode: 'monthly-unique-inherit-persistence', currentYear: 209, currentMonth: 12, tickSeconds: 600, config: { stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 75, npcMin: 10, chiefMin: 70 }, iconPath: '.', map: {}, const: {}, environment: { mapName: 'che', unitSet: 'che' }, }, meta: { hiddenSeed: 'monthly-unique-inherit-persistence' }, }, }); const state: TurnWorldState = { id: worldRow.id, currentYear: 209, currentMonth: 12, tickSeconds: 600, lastTurnTime: general.turnTime, meta: { hiddenSeed: 'monthly-unique-inherit-persistence' }, }; const loaded = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! }); const loadedGeneral = loaded.snapshot.generals.find((entry) => entry.id === generalId); expect(loadedGeneral?.meta).toEqual( expect.objectContaining({ rank_warnum: 2, firenum: 1, betwin: 2, betgold: 4000, betwingold: 2000, inherit_earned_act: 11, inherit_spent_dyn: 9, }) ); expect(loadedGeneral?.inheritancePoints).toEqual(general.inheritancePoints); const snapshot: TurnWorldSnapshot = { scenarioConfig: { stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 75, npcMin: 10, chiefMin: 70 }, iconPath: '.', map: {}, const: {}, environment: { mapName: 'test', unitSet: 'default' }, }, map: { id: 'test', name: 'test', cities: [] }, generals: [general], cities: [], nations: [nation], troops: [], diplomacy: [], events: [event], initialEvents: [], }; const modules = (await loadActionModuleBundle()).itemModules; let world: InMemoryTurnWorld | null = null; const lost = createLostUniqueItemHandler({ getWorld: () => world, itemModules: modules }); const merge = createMergeInheritPointRankHandler({ getWorld: () => world }); world = new InMemoryTurnWorld(state, snapshot, { schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] }, calendarHandler: createMonthlyEventHandler({ getWorld: () => world, startYear: 180, actions: new Map([ ['LostUniqueItem', lost], ['MergeInheritPointRank', merge], ]), }), }); const hooks = await createDatabaseTurnHooks(databaseUrl!, world); try { await world.advanceMonth(new Date('0210-01-01T00:00:00.000Z')); await hooks.hooks.flushChanges?.({ lastTurnTime: state.lastTurnTime.toISOString(), processedGenerals: 0, processedTurns: 2, durationMs: 0, partial: false, }); const row = await db.general.findUniqueOrThrow({ where: { id: generalId } }); expect(row).toMatchObject({ horseCode: 'None', weaponCode: 'che_무기_01_단도', itemCode: 'None', }); expect(row.meta).toHaveProperty('itemInventory'); expect( await db.rankData.findMany({ where: { generalId, type: { in: [ 'inherit_earned', 'inherit_earned_act', 'inherit_earned_dyn', 'inherit_spent', 'inherit_spent_dyn', ], }, }, orderBy: { type: 'asc' }, select: { type: true, value: true }, }) ).toEqual([ { type: 'inherit_earned', value: 111 }, { type: 'inherit_earned_act', value: 11 }, { type: 'inherit_earned_dyn', value: 100 }, { type: 'inherit_spent', value: 9 }, { type: 'inherit_spent_dyn', value: 9 }, ]); expect(await db.logEntry.count({ where: { generalId } })).toBe(2); } finally { await hooks.close(); await db.worldState.deleteMany({ where: { id: worldRow.id } }); } }); });