import { describe, expect, it } from 'vitest'; import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic'; import { applyRealtimeReadModelBaseline, createRealtimeReadModelBaseline, summarizeRealtimeReadModelChanges, } from '../src/turn/databaseHooks.js'; import type { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js'; import type { TurnWorldChanges } from '../src/turn/inMemoryWorld.js'; import type { ReservedTurnChanges } from '../src/turn/reservedTurnStore.js'; describe('summarizeRealtimeReadModelChanges', () => { it('emits deterministic entity and record invalidations from committed changes', () => { const worldChanges = { generals: [{ id: 9 }, { id: 7 }], createdGenerals: [{ id: 8 }], deletedGenerals: [9], cities: [{ id: 4 }], nations: [{ id: 3 }], createdNations: [], deletedNations: [], deletedNationSnapshots: [], lifecycleEvents: [], logs: [ { scope: LogScope.GENERAL, category: LogCategory.ACTION, generalId: 7, format: LogFormat.PLAIN, text: 'general', }, { scope: LogScope.SYSTEM, category: LogCategory.SUMMARY, format: LogFormat.PLAIN, text: 'summary', }, { scope: LogScope.SYSTEM, category: LogCategory.HISTORY, format: LogFormat.PLAIN, text: 'history', }, ], } as unknown as TurnWorldChanges; const reservedChanges: ReservedTurnChanges = { generalIds: [9], generalInitializationIds: [7], generalLeaseIds: [9, 8], nationKeys: [], nationInitializationKeys: [], nationLeaseKeys: [], }; expect(summarizeRealtimeReadModelChanges(worldChanges, reservedChanges)).toEqual({ generalIds: [7, 8, 9], cityIds: [4], nationIds: [3], mapGeneralIds: [7, 8, 9], mapCityIds: [4], mapNationIds: [3], frontStatusGeneralIds: [7, 8, 9], frontStatusNationIds: [3], lobbyGeneralIds: [7, 8, 9], reservedGeneralIds: [7, 8, 9], recordGeneralIds: [7], worldChanged: false, globalRecordsChanged: true, worldHistoryChanged: true, contactsChanged: true, frontStatusChanged: false, lobbyChanged: true, }); }); it('separates content changes from map and contact projections', () => { const baseline = createRealtimeReadModelBaseline({ listGenerals: () => [ { id: 7, name: '장수', cityId: 1, nationId: 1, officerLevel: 1, npcState: 0, gold: 100, meta: { permission: 'normal' }, penalty: {}, }, ], listCities: () => [ { id: 1, level: 1, nationId: 1, state: 0, supplyState: 1, population: 100 }, ], listNations: () => [ { id: 1, name: '위', color: '#008000', capitalCityId: 1, gold: 100 }, ], } as unknown as InMemoryTurnWorld); const changes = { generals: [ { id: 7, name: '장수', cityId: 1, nationId: 1, officerLevel: 1, npcState: 0, gold: 90, meta: { permission: 'normal' }, penalty: {}, }, ], createdGenerals: [], deletedGenerals: [], cities: [{ id: 1, level: 1, nationId: 1, state: 0, supplyState: 1, population: 101 }], nations: [{ id: 1, name: '위', color: '#008000', capitalCityId: 1, gold: 90 }], createdNations: [], deletedNations: [], deletedNationSnapshots: [], lifecycleEvents: [], logs: [], } as unknown as TurnWorldChanges; expect(summarizeRealtimeReadModelChanges(changes, undefined, baseline)).toMatchObject({ generalIds: [7], cityIds: [1], nationIds: [1], mapGeneralIds: [], mapCityIds: [], mapNationIds: [], frontStatusGeneralIds: [], frontStatusNationIds: [], lobbyGeneralIds: [], contactsChanged: false, frontStatusChanged: false, lobbyChanged: false, }); applyRealtimeReadModelBaseline(baseline, changes); expect(summarizeRealtimeReadModelChanges(changes, undefined, baseline)).toMatchObject({ generalIds: [], cityIds: [], nationIds: [], mapGeneralIds: [], mapCityIds: [], mapNationIds: [], frontStatusGeneralIds: [], frontStatusNationIds: [], lobbyGeneralIds: [], contactsChanged: false, frontStatusChanged: false, lobbyChanged: false, }); }); it('detects map, contact, and front-status fields independently', () => { const baseline = createRealtimeReadModelBaseline({ listGenerals: () => [ { id: 7, name: '장수', cityId: 1, nationId: 1, officerLevel: 1, npcState: 0, meta: { permission: 'normal' }, penalty: {}, }, ], listCities: () => [{ id: 1, level: 1, nationId: 1, state: 0, supplyState: 1 }], listNations: () => [ { id: 1, name: '위', color: '#008000', capitalCityId: 1, meta: { notice: '이전' } }, ], } as unknown as InMemoryTurnWorld); const changes = { generals: [ { id: 7, name: '장수', cityId: 1, nationId: 1, officerLevel: 5, npcState: 0, meta: { permission: 'normal' }, penalty: {}, }, ], createdGenerals: [], deletedGenerals: [], cities: [{ id: 1, level: 2, nationId: 1, state: 0, supplyState: 1 }], nations: [ { id: 1, name: '위', color: '#008000', capitalCityId: 1, meta: { notice: '새 방침' } }, ], createdNations: [], deletedNations: [], deletedNationSnapshots: [], lifecycleEvents: [], logs: [], } as unknown as TurnWorldChanges; expect(summarizeRealtimeReadModelChanges(changes, undefined, baseline)).toMatchObject({ generalIds: [7], cityIds: [1], nationIds: [1], mapGeneralIds: [], mapCityIds: [1], mapNationIds: [], frontStatusGeneralIds: [], frontStatusNationIds: [1], lobbyGeneralIds: [], contactsChanged: true, frontStatusChanged: false, lobbyChanged: false, }); }); it('separates global front-status names from nation-targeted notices', () => { const baseline = createRealtimeReadModelBaseline({ listGenerals: () => [ { id: 7, name: '장수', cityId: 1, nationId: 1, officerLevel: 1, npcState: 0, meta: {}, penalty: {}, }, ], listCities: () => [], listNations: () => [ { id: 1, name: '위', color: '#008000', capitalCityId: 1, meta: { notice: '이전' } }, ], } as unknown as InMemoryTurnWorld); const changes = { generals: [ { id: 7, name: '새 장수', cityId: 1, nationId: 1, officerLevel: 1, npcState: 0, meta: {}, penalty: {}, }, ], createdGenerals: [], deletedGenerals: [], cities: [], nations: [ { id: 1, name: '촉', color: '#008000', capitalCityId: 1, meta: { notice: '새 공지' } }, ], createdNations: [], deletedNations: [], deletedNationSnapshots: [], lifecycleEvents: [], logs: [], } as unknown as TurnWorldChanges; expect(summarizeRealtimeReadModelChanges(changes, undefined, baseline)).toMatchObject({ frontStatusGeneralIds: [7], frontStatusNationIds: [1], frontStatusChanged: true, }); }); });