Files
core2026/app/gateway-api/test/profileStatusService.test.ts
T
Hide_D 14ffa76c49 feat: 예약 오픈 일정을 빌드 전에 공개
초기화 예약의 공개 선택을 operation payload에 저장하고 로비에서 준비 단계별로 표시합니다. RESERVED 인계와 STOPPED 무요청 경계를 테스트합니다.
2026-08-23 13:59:31 +00:00

105 lines
4.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { resolveUpcomingResetAnnouncement, shouldExposeUpcomingReset } from '../src/lobby/profileStatusService.js';
import type { GatewayOperationRecord } from '../src/orchestrator/profileRepository.js';
const buildOperation = (status: GatewayOperationRecord['status'] = 'QUEUED'): GatewayOperationRecord => ({
id: '11111111-1111-4111-8111-111111111111',
profileName: 'che:2',
type: 'RESET',
status,
sourceMode: 'BRANCH',
sourceRef: 'private/source-ref',
payload: {
install: { scenarioId: 1010 },
requestedSource: 'CURRENT',
publicAnnouncement: {
enabled: true,
scenarioId: 1010,
scenarioTitle: '황건적의 난',
scheduledAt: '2026-08-27T05:00:00.000Z',
preopenAt: '2026-08-27T05:30:00.000Z',
openAt: '2026-08-27T11:00:00.000Z',
turnTermMinutes: 60,
fictionMode: '가상',
npcMode: 1,
defaultStatTotal: 70,
otherTextInfo: '랜덤 임관',
autorunUser: {
limitMinutes: 1440,
options: ['develop', 'battle'],
},
requestedBy: 'must-not-leak',
reason: 'must-not-leak',
},
},
reason: 'private reason',
requestedBy: 'admin-id',
scheduledAt: '2026-08-27T05:00:00.000Z',
error: 'private error',
createdAt: '2026-08-23T00:00:00.000Z',
updatedAt: '2026-08-23T00:00:00.000Z',
});
describe('resolveUpcomingResetAnnouncement', () => {
it('projects only the public snapshot while the delayed build is queued', () => {
const result = resolveUpcomingResetAnnouncement(buildOperation(), new Date('2026-08-27T04:00:00.000Z'));
expect(result).toEqual({
phase: 'SCHEDULED',
scheduledAt: '2026-08-27T05:00:00.000Z',
preopenAt: '2026-08-27T05:30:00.000Z',
openAt: '2026-08-27T11:00:00.000Z',
scenarioId: 1010,
scenarioTitle: '황건적의 난',
turnTermMinutes: 60,
fictionMode: '가상',
npcMode: 1,
defaultStatTotal: 70,
otherTextInfo: '랜덤 임관',
autorunUser: {
limitMinutes: 1440,
options: ['develop', 'battle'],
},
});
expect(result).not.toHaveProperty('sourceRef');
expect(result).not.toHaveProperty('requestedBy');
expect(result).not.toHaveProperty('reason');
expect(result).not.toHaveProperty('error');
});
it('moves from preparation to a truthful delay state without changing the profile lifecycle', () => {
expect(
resolveUpcomingResetAnnouncement(buildOperation('RUNNING'), new Date('2026-08-27T05:10:00.000Z'))
).toMatchObject({ phase: 'PREPARING' });
expect(
resolveUpcomingResetAnnouncement(buildOperation('RUNNING'), new Date('2026-08-27T05:31:00.000Z'))
).toMatchObject({ phase: 'DELAYED' });
});
it('keeps a completed build ready for RESERVED handoff and removes cancelled or failed announcements', () => {
const succeeded = buildOperation('SUCCEEDED');
expect(resolveUpcomingResetAnnouncement(succeeded, new Date('2026-08-27T05:20:00.000Z'))).toMatchObject({
phase: 'READY',
});
expect(shouldExposeUpcomingReset(succeeded, 'RESERVED')).toBe(true);
expect(shouldExposeUpcomingReset(succeeded, 'PREOPEN')).toBe(false);
expect(shouldExposeUpcomingReset(succeeded, 'RUNNING')).toBe(false);
for (const status of ['CANCELLED', 'FAILED'] as const) {
expect(
resolveUpcomingResetAnnouncement(buildOperation(status), new Date('2026-08-27T04:00:00.000Z'))
).toBeNull();
}
});
it('fails closed for an unpublished or incomplete snapshot', () => {
const unpublished = buildOperation();
unpublished.payload = { publicAnnouncement: { enabled: false } };
expect(resolveUpcomingResetAnnouncement(unpublished, new Date('2026-08-27T04:00:00.000Z'))).toBeNull();
const incomplete = buildOperation();
incomplete.payload = { publicAnnouncement: { enabled: true, scenarioTitle: '황건적의 난' } };
expect(resolveUpcomingResetAnnouncement(incomplete, new Date('2026-08-27T04:00:00.000Z'))).toBeNull();
});
});