Files
core2026/packages/common/test/tournamentSourceRevision.test.ts
T
Hide_D 985f627499 feat: 토너먼트 페이지군 자동 갱신을 분리한다
토너먼트 전용 SSE scope와 slice invalidation을 추가하고 같은 계정의 visible 탭이 조회를 공유하게 한다. 서버 증명 snapshot 조회는 접속 점수를 더하지 않되 기존 hard limit은 유지한다.
2026-08-22 06:00:22 +00:00

161 lines
5.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { writeTournamentProjection } from '../src/tournament/sourceRevision.js';
const keys = {
stateKey: 'state',
participantsKey: 'participants',
matchesKey: 'matches',
bettingKey: 'betting',
sourceRevisionKey: 'revision',
sourceRevisionChannel: 'changed',
realtimeEventChannel: 'realtime',
};
describe('tournament source revision', () => {
it('passes every payload and one profile revision key to a single atomic script', async () => {
const calls: Array<{ keys: string[]; arguments: string[] }> = [];
const published: Array<{ channel: string; message: string }> = [];
const redis = {
eval: async (_script: string, options: { keys: string[]; arguments: string[] }) => {
calls.push(options);
return '7:1';
},
publish: async (channel: string, message: string) => {
published.push({ channel, message });
return 1;
},
};
await expect(
writeTournamentProjection(redis, keys, [
{ key: 'state', value: { stage: 1 } },
{ key: 'matches', value: [] },
])
).resolves.toBe('7');
expect(calls).toEqual([
{
keys: ['state', 'matches', 'revision'],
arguments: [JSON.stringify({ stage: 1 }), '[]'],
},
]);
expect(published).toEqual([
{ channel: 'changed', message: JSON.stringify({ sourceRevision: '7' }) },
{
channel: 'realtime',
message: JSON.stringify({
type: 'tournamentProjectionChanged',
invalidation: { snapshot: true, betting: true, rankings: false },
}),
},
{ channel: 'realtime', message: JSON.stringify({ type: 'tournamentChanged' }) },
]);
});
it('rejects empty or duplicate writes before evaluating Redis', async () => {
const redis = { eval: async () => '1' };
await expect(writeTournamentProjection(redis, keys, [])).rejects.toThrow('at least one');
await expect(
writeTournamentProjection(redis, keys, [
{ key: 'state', value: 1 },
{ key: 'state', value: 2 },
])
).rejects.toThrow('unique');
});
it('does not wake the main dashboard for participant-only writes', async () => {
const published: string[] = [];
const redis = {
eval: async () => '8',
publish: async (channel: string) => {
published.push(channel);
return 1;
},
};
await writeTournamentProjection(redis, keys, [{ key: 'participants', value: [{ id: 7 }] }]);
expect(published).toEqual(['changed', 'realtime']);
});
it('does not wake the main dashboard when tournament state keeps the same stage', async () => {
const published: string[] = [];
const redis = {
eval: async () => '10:0',
publish: async (channel: string) => {
published.push(channel);
return 1;
},
};
await expect(
writeTournamentProjection(redis, keys, [{ key: 'state', value: { stage: 1, phase: 2 } }])
).resolves.toBe('10');
expect(published).toEqual(['changed', 'realtime']);
});
it('keeps both post-commit wake-up channels independently best effort', async () => {
const published: string[] = [];
const redis = {
eval: async () => '9',
publish: async (channel: string) => {
published.push(channel);
if (channel === 'changed') throw new Error('source subscriber unavailable');
return 1;
},
};
await expect(writeTournamentProjection(redis, keys, [{ key: 'state', value: { stage: 1 } }])).resolves.toBe(
'9'
);
expect(published).toEqual(['changed', 'realtime', 'realtime']);
});
it('selects only the betting slice for a same-stage bet write', async () => {
const published: Array<{ channel: string; message: string }> = [];
const redis = {
eval: async () => '11:0',
publish: async (channel: string, message: string) => {
published.push({ channel, message });
return 1;
},
};
await writeTournamentProjection(redis, keys, [{ key: 'betting', value: [{ targetId: 3, amount: 10 }] }]);
expect(published.at(-1)).toEqual({
channel: 'realtime',
message: JSON.stringify({
type: 'tournamentProjectionChanged',
invalidation: { snapshot: false, betting: true, rankings: false },
}),
});
expect(published.some(({ message }) => message.includes('tournamentChanged'))).toBe(false);
});
it('refreshes rankings after reward persistence without waking the main dashboard', async () => {
const published: Array<{ channel: string; message: string }> = [];
const redis = {
eval: async () => '12:0',
publish: async (channel: string, message: string) => {
published.push({ channel, message });
return 1;
},
};
await writeTournamentProjection(redis, keys, [
{ key: 'state', value: { stage: 0, rewardSettled: true, bettingSettled: false } },
]);
expect(published.at(-1)).toEqual({
channel: 'realtime',
message: JSON.stringify({
type: 'tournamentProjectionChanged',
invalidation: { snapshot: true, betting: false, rankings: true },
}),
});
expect(published.some(({ message }) => message.includes('tournamentChanged'))).toBe(false);
});
});