fix: 시즌별 설문 알림 상태를 분리
설문 확인 cursor와 메인 실시간 탭 범위를 reset 고유 serverId로 구분한다. 설문 생성의 daemon 비의존 outbox 경로와 이전 시즌 cursor 회귀를 테스트한다.
This commit is contained in:
@@ -43,6 +43,7 @@ export const zWorldStateConfig = z.object({
|
||||
export type WorldStateConfig = z.infer<typeof zWorldStateConfig>;
|
||||
|
||||
export const zWorldStateMeta = z.object({
|
||||
serverId: z.string().optional(),
|
||||
starttime: z.string().optional(),
|
||||
opentime: z.string().optional(),
|
||||
preopenAt: z.string().optional(),
|
||||
|
||||
@@ -885,11 +885,16 @@ export const generalRouter = router({
|
||||
})
|
||||
: null;
|
||||
const worldMeta = asRecord(worldState.meta);
|
||||
const serverId =
|
||||
typeof worldMeta.serverId === 'string' && worldMeta.serverId.trim()
|
||||
? worldMeta.serverId.trim()
|
||||
: ctx.profile?.name || 'game';
|
||||
const rawLastExecuted = worldMeta.lastTurnTime ?? worldMeta.turntime;
|
||||
const parsedLastExecuted =
|
||||
typeof rawLastExecuted === 'string' || rawLastExecuted instanceof Date ? new Date(rawLastExecuted) : null;
|
||||
|
||||
return {
|
||||
serverId,
|
||||
onlineUserCount: onlineGenerals.length,
|
||||
onlineNations,
|
||||
onlineGenerals: myOnlineGenerals,
|
||||
|
||||
@@ -52,6 +52,7 @@ export const lobbyRouter = router({
|
||||
}
|
||||
|
||||
return {
|
||||
serverId: worldState.meta.serverId?.trim() || ctx.profile?.name || 'game',
|
||||
year: worldState.currentYear,
|
||||
month: worldState.currentMonth,
|
||||
userCnt,
|
||||
|
||||
@@ -74,6 +74,7 @@ describe('lobby season state', () => {
|
||||
.createCaller(
|
||||
buildContext(
|
||||
{
|
||||
serverId: 'che_260819_season',
|
||||
preopenAt: '2026-08-19 22:00:00',
|
||||
opentime: '2026-08-19 23:00:00',
|
||||
scenarioMeta: { title: '【가상모드27-b】 아시아 명장전(비급)' },
|
||||
@@ -101,6 +102,7 @@ describe('lobby season state', () => {
|
||||
.lobby.info();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
serverId: 'che_260819_season',
|
||||
preopenAt: '2026-08-19 22:00:00',
|
||||
opentime: '2026-08-19 23:00:00',
|
||||
scenarioTitle: '【가상모드27-b】 아시아 명장전(비급)',
|
||||
|
||||
@@ -40,6 +40,7 @@ const buildContext = (options: { auth?: GameSessionTokenPayload | null; hasVoted
|
||||
findFirst: vi.fn(async () => ({
|
||||
tickSeconds: 3600,
|
||||
meta: {
|
||||
serverId: 'che_260819_front',
|
||||
lastTurnTime: '2026-07-26T10:00:00.000Z',
|
||||
},
|
||||
})),
|
||||
@@ -87,6 +88,7 @@ describe('general.getFrontStatus', () => {
|
||||
const result = await caller.general.getFrontStatus();
|
||||
|
||||
expect(result).toEqual({
|
||||
serverId: 'che_260819_front',
|
||||
onlineUserCount: 3,
|
||||
onlineNations: '【촉】, 【위】',
|
||||
onlineGenerals: '유비, 관우',
|
||||
|
||||
@@ -6,10 +6,23 @@ import type { ReadModelOutboxDatabase } from '@sammo-ts/infra';
|
||||
import { ReadModelOutboxWorker } from '../src/realtime/outboxWorker.js';
|
||||
|
||||
const payload = (
|
||||
domain: 'front.general' | 'access.general' | 'dashboard.global' | 'messages.mailbox' | 'tournament' | 'betting'
|
||||
domain:
|
||||
| 'front.global'
|
||||
| 'front.general'
|
||||
| 'access.general'
|
||||
| 'dashboard.global'
|
||||
| 'messages.mailbox'
|
||||
| 'tournament'
|
||||
| 'betting'
|
||||
) => ({
|
||||
version: 1,
|
||||
changes: [[domain, domain === 'front.general' || domain === 'access.general' ? 7 : domain === 'messages.mailbox' ? 9999 : 0, '1']],
|
||||
changes: [
|
||||
[
|
||||
domain,
|
||||
domain === 'front.general' || domain === 'access.general' ? 7 : domain === 'messages.mailbox' ? 9999 : 0,
|
||||
'1',
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const createFixture = (rows: readonly object[]) => {
|
||||
@@ -26,6 +39,23 @@ const createFixture = (rows: readonly object[]) => {
|
||||
};
|
||||
|
||||
describe('ReadModelOutboxWorker', () => {
|
||||
it('publishes a survey-style global front-status invalidation without a turn daemon', async () => {
|
||||
const fixture = createFixture([{ id: 10n, payload: payload('front.global'), attempts: 1 }]);
|
||||
const worker = new ReadModelOutboxWorker(fixture.db, fixture.redis, 'che:default', {
|
||||
owner: 'worker-test',
|
||||
intervalMs: 60_000,
|
||||
});
|
||||
|
||||
worker.start();
|
||||
await vi.waitFor(() => expect(fixture.updateMany).toHaveBeenCalledTimes(1));
|
||||
await worker.stop();
|
||||
|
||||
expect(JSON.parse(String(fixture.publish.mock.calls[0]?.[1]))).toMatchObject({
|
||||
type: 'readModelChanged',
|
||||
changes: { frontStatusChanged: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('publishes a legacy internal readModelChanged event and acknowledges the durable row', async () => {
|
||||
const fixture = createFixture([{ id: 11n, payload: payload('front.general'), attempts: 1 }]);
|
||||
const worker = new ReadModelOutboxWorker(fixture.db, fixture.redis, 'che:default', {
|
||||
|
||||
@@ -241,6 +241,7 @@ describe('vote router actor and permission boundaries', () => {
|
||||
).resolves.toEqual({ ok: true });
|
||||
|
||||
expect(fixture.changeJournal.snapshot()).toEqual([{ domain: 'front.global', entityId: 0 }]);
|
||||
expect(fixture.requestCommand).not.toHaveBeenCalled();
|
||||
expect(fixture.redisIncr).not.toHaveBeenCalled();
|
||||
expect(fixture.redisPublish).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user