Merge remote-tracking branch 'origin/main' into fix/existing-test-failures-20260812
This commit is contained in:
@@ -156,7 +156,8 @@ describe('archive.myPastPlays', () => {
|
||||
leadership: 80,
|
||||
strength: 70,
|
||||
officerLevel: 8,
|
||||
personal: '3',
|
||||
officerLevelText: '제2장군',
|
||||
personal: '-',
|
||||
historyCount: 2,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -166,9 +167,10 @@ describe('archive.myPastPlays', () => {
|
||||
strength: 71,
|
||||
intel: 61,
|
||||
officerLevel: 7,
|
||||
personal: 'che_의리',
|
||||
special: 'che_상재',
|
||||
special2: 'che_신산',
|
||||
officerLevelText: '제2모사',
|
||||
personal: '의리',
|
||||
special: '상재',
|
||||
special2: '신산',
|
||||
historyCount: 1,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
resolveCityLevelName,
|
||||
resolveDedicationLevelName,
|
||||
resolveNationLevelName,
|
||||
resolveOfficerLevelName,
|
||||
resolveRegionName,
|
||||
sanitizeInternalDisplayCode,
|
||||
} from '../src/services/gameDisplayNames.js';
|
||||
|
||||
describe('Ref GUI display names', () => {
|
||||
it('maps nation, city, region, office, and dedication levels to Ref labels', () => {
|
||||
expect(resolveNationLevelName(3)).toBe('주자사');
|
||||
expect(resolveCityLevelName(8)).toBe('특');
|
||||
expect(resolveRegionName(2)).toBe('중원');
|
||||
expect(resolveOfficerLevelName(9, 3)).toBe('간의대부');
|
||||
expect(resolveOfficerLevelName(5, 3)).toBe('-');
|
||||
expect(resolveOfficerLevelName(5)).toBe('제3모사');
|
||||
expect(resolveDedicationLevelName(2, 30)).toBe('29품관');
|
||||
expect(resolveDedicationLevelName(0, 30)).toBe('무품관');
|
||||
});
|
||||
|
||||
it('never exposes internal prefixes or numeric fallback codes', () => {
|
||||
expect(sanitizeInternalDisplayCode('che_event_의병')).toBe('의병');
|
||||
expect(sanitizeInternalDisplayCode('che_법가')).toBe('법가');
|
||||
expect(sanitizeInternalDisplayCode('3')).toBe('-');
|
||||
expect(resolveNationLevelName(99)).toBe('-');
|
||||
expect(resolveCityLevelName(99)).toBe('-');
|
||||
expect(resolveRegionName(99)).toBe('-');
|
||||
});
|
||||
});
|
||||
@@ -248,6 +248,55 @@ describe('in-game my information ownership', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns Ref display names instead of numeric levels and internal codes for the main GUI', async () => {
|
||||
const fixture = createContext({
|
||||
me: buildGeneral({
|
||||
officerLevel: 9,
|
||||
crewTypeId: 1100,
|
||||
horseCode: 'che_명마_03_노새',
|
||||
meta: { explevel: 4, dedlevel: 2 },
|
||||
}),
|
||||
city: {
|
||||
id: 1,
|
||||
name: '업',
|
||||
level: 8,
|
||||
nationId: 1,
|
||||
population: 1_000,
|
||||
populationMax: 2_000,
|
||||
agriculture: 100,
|
||||
agricultureMax: 200,
|
||||
commerce: 100,
|
||||
commerceMax: 200,
|
||||
security: 100,
|
||||
securityMax: 200,
|
||||
trust: 70,
|
||||
trade: 100,
|
||||
defence: 100,
|
||||
defenceMax: 200,
|
||||
wall: 100,
|
||||
wallMax: 200,
|
||||
region: 2,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({
|
||||
general: {
|
||||
officerLevelText: '간의대부',
|
||||
crewTypeName: '보병',
|
||||
progression: { experienceLevel: 4, dedicationLevel: 2, dedicationText: '29품관' },
|
||||
itemNames: { horse: '노새(+3)' },
|
||||
},
|
||||
city: { levelName: '특', regionName: '중원', nationName: '위' },
|
||||
nation: {
|
||||
levelName: '주자사',
|
||||
typeName: '법가',
|
||||
capitalCityName: '업',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the Ref-style neutral nation frame and trait display names on the main read model', async () => {
|
||||
const fixture = createContext({
|
||||
me: buildGeneral({
|
||||
@@ -542,8 +591,14 @@ describe('battle-center general and user permissions', () => {
|
||||
id: 7,
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
officerLevelText: '일반',
|
||||
crewTypeName: '-',
|
||||
equipmentNames: { weapon: '-', book: '-', horse: '-', item: '-' },
|
||||
traits: { personal: '-', specialDomestic: '-', specialWar: '-' },
|
||||
progression: {
|
||||
experienceLevel: 0,
|
||||
dedicationLevel: 1,
|
||||
dedicationText: '30품관',
|
||||
statExperience: { leadership: 0, strength: 0, intelligence: 0 },
|
||||
statUpgradeLimit: 20,
|
||||
dex: [0, 0, 0, 0, 0],
|
||||
|
||||
@@ -117,6 +117,9 @@ describe('nation general and secret office permissions', () => {
|
||||
cityName: null,
|
||||
troopName: null,
|
||||
refreshScoreTotal: 10,
|
||||
dedicationLevel: 1,
|
||||
dedicationText: '30품관',
|
||||
bill: 600,
|
||||
});
|
||||
expect(result.generals[0]).not.toHaveProperty('crew');
|
||||
await expect(caller.nation.getSecretGeneralList()).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createEmptyRealtimeReadModelChanges, type RealtimeEvent } from '@sammo-ts/common';
|
||||
import { MESSAGE_MAILBOX_NATIONAL_BASE } from '@sammo-ts/logic';
|
||||
|
||||
import { shouldReloadRealtimeViewerIdentity, toPublicRealtimeEvent } from '../src/realtime/publicEvent.js';
|
||||
|
||||
const viewer = { generalId: 7, cityId: 3, nationId: 2 } as const;
|
||||
|
||||
const turnEvent = (changes = createEmptyRealtimeReadModelChanges()): RealtimeEvent => ({
|
||||
type: 'turnCompleted',
|
||||
at: '2026-08-12T12:34:56.789Z',
|
||||
lastTurnTime: '0185-02-01T00:00:00.000Z',
|
||||
changes,
|
||||
revision: 42,
|
||||
});
|
||||
|
||||
describe('public realtime event privacy boundary', () => {
|
||||
it('suppresses clock-only and unrelated private general turns', () => {
|
||||
expect(toPublicRealtimeEvent(turnEvent(), [viewer])).toBeNull();
|
||||
expect(
|
||||
toPublicRealtimeEvent(
|
||||
turnEvent({
|
||||
...createEmptyRealtimeReadModelChanges(),
|
||||
generalIds: [99],
|
||||
}),
|
||||
[viewer]
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('publishes only viewer-specific boolean invalidations', () => {
|
||||
const publicEvent = toPublicRealtimeEvent(
|
||||
turnEvent({
|
||||
...createEmptyRealtimeReadModelChanges(),
|
||||
generalIds: [7, 99],
|
||||
reservedGeneralIds: [7],
|
||||
recordGeneralIds: [7],
|
||||
}),
|
||||
[viewer]
|
||||
);
|
||||
|
||||
expect(publicEvent).toEqual({
|
||||
type: 'readModelInvalidated',
|
||||
invalidation: {
|
||||
context: true,
|
||||
lobby: false,
|
||||
map: false,
|
||||
commands: true,
|
||||
contacts: false,
|
||||
boardAccess: true,
|
||||
reservedTurns: true,
|
||||
records: true,
|
||||
frontStatus: false,
|
||||
},
|
||||
});
|
||||
const serialized = JSON.stringify(publicEvent);
|
||||
expect(publicEvent).not.toHaveProperty('at');
|
||||
expect(publicEvent).not.toHaveProperty('lastTurnTime');
|
||||
expect(publicEvent).not.toHaveProperty('revision');
|
||||
for (const forbidden of ['generalIds', 'cityIds', 'nationIds', '99']) {
|
||||
expect(serialized).not.toContain(forbidden);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps global refresh meaning without exposing its source identity or time', () => {
|
||||
const publicEvent = toPublicRealtimeEvent(
|
||||
{
|
||||
type: 'readModelChanged',
|
||||
at: '2026-08-12T12:34:56.789Z',
|
||||
revision: 43,
|
||||
changes: {
|
||||
...createEmptyRealtimeReadModelChanges(),
|
||||
worldChanged: true,
|
||||
globalRecordsChanged: true,
|
||||
worldHistoryChanged: true,
|
||||
},
|
||||
},
|
||||
[viewer]
|
||||
);
|
||||
|
||||
expect(publicEvent).toMatchObject({
|
||||
type: 'readModelInvalidated',
|
||||
invalidation: { lobby: true, map: true, commands: true, records: true },
|
||||
});
|
||||
expect(JSON.stringify(publicEvent)).not.toMatch(/2026|0185|revision|Ids/u);
|
||||
});
|
||||
|
||||
it('uses a conservative identifier-free fallback for an older daemon', () => {
|
||||
expect(
|
||||
toPublicRealtimeEvent(
|
||||
{
|
||||
type: 'turnCompleted',
|
||||
at: '2026-08-12T12:34:56.789Z',
|
||||
lastTurnTime: '0185-02-01T00:00:00.000Z',
|
||||
},
|
||||
[viewer]
|
||||
)
|
||||
).toEqual({
|
||||
type: 'readModelInvalidated',
|
||||
invalidation: {
|
||||
context: true,
|
||||
lobby: true,
|
||||
map: true,
|
||||
commands: true,
|
||||
contacts: true,
|
||||
boardAccess: true,
|
||||
reservedTurns: true,
|
||||
records: true,
|
||||
frontStatus: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('filters message events per viewer and removes mailbox, sender, message, and time fields', () => {
|
||||
const event: RealtimeEvent = {
|
||||
type: 'messageCreated',
|
||||
at: '2026-08-12T12:34:56.789Z',
|
||||
mailbox: MESSAGE_MAILBOX_NATIONAL_BASE + viewer.nationId,
|
||||
msgType: 'national',
|
||||
messageId: 123,
|
||||
senderId: 99,
|
||||
};
|
||||
|
||||
expect(toPublicRealtimeEvent(event, [viewer])).toEqual({ type: 'messagesInvalidated' });
|
||||
expect(toPublicRealtimeEvent({ ...event, mailbox: MESSAGE_MAILBOX_NATIONAL_BASE + 8 }, [viewer])).toBeNull();
|
||||
});
|
||||
|
||||
it('requests an identity refresh only when the viewer general may have changed', () => {
|
||||
expect(
|
||||
shouldReloadRealtimeViewerIdentity(
|
||||
turnEvent({ ...createEmptyRealtimeReadModelChanges(), generalIds: [7] }),
|
||||
viewer
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldReloadRealtimeViewerIdentity(
|
||||
turnEvent({ ...createEmptyRealtimeReadModelChanges(), generalIds: [99] }),
|
||||
viewer
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('merges previous and committed identities across an ownership transition', () => {
|
||||
const event: RealtimeEvent = {
|
||||
type: 'readModelChanged',
|
||||
at: '2026-08-12T12:34:56.789Z',
|
||||
revision: 44,
|
||||
changes: {
|
||||
...createEmptyRealtimeReadModelChanges(),
|
||||
generalIds: [7],
|
||||
nationIds: [3],
|
||||
frontStatusNationIds: [3],
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
toPublicRealtimeEvent(event, [viewer, { generalId: 7, cityId: 4, nationId: 3 }])
|
||||
).toMatchObject({
|
||||
type: 'readModelInvalidated',
|
||||
invalidation: {
|
||||
context: true,
|
||||
commands: true,
|
||||
boardAccess: true,
|
||||
frontStatus: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user