Files
core2026/app/game-engine/test/realtimeReadModelChanges.test.ts
T

269 lines
9.3 KiB
TypeScript

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,
});
});
});