From 86e6a283df3107e066d79c30176f5d35e1c4d1f6 Mon Sep 17 00:00:00 2001 From: hided62 Date: Thu, 27 Aug 2026 01:36:50 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=B5=9C=EC=8B=A0=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EA=B3=B5=EC=A7=80=EB=A7=8C=20=EB=A1=9C=EB=B9=84?= =?UTF-8?q?=EC=97=90=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 새 초기화가 이전 예약 공지를 대체하면 과거 publicAnnouncement로 되돌아가지 않도록 프로필별 최신 RESET을 공지 경계로 사용한다. 취소된 최신 작업과 즉시 초기화 회귀 테스트를 추가한다. --- .../src/lobby/profileStatusService.ts | 26 +++++++--- .../test/profileStatusService.test.ts | 50 ++++++++++++++++++- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/app/gateway-api/src/lobby/profileStatusService.ts b/app/gateway-api/src/lobby/profileStatusService.ts index ed2591e7..be36bc77 100644 --- a/app/gateway-api/src/lobby/profileStatusService.ts +++ b/app/gateway-api/src/lobby/profileStatusService.ts @@ -98,7 +98,6 @@ export class RepositoryProfileStatusService implements GatewayProfileStatusServi const [profileRows, recentResetOperations] = await Promise.all([ this.profiles.listProfiles(), this.profiles.listOperations({ - statuses: ['QUEUED', 'RUNNING', 'SUCCEEDED'], types: ['RESET'], limit: 200, }), @@ -106,15 +105,9 @@ export class RepositoryProfileStatusService implements GatewayProfileStatusServi const rows = orderGatewayProfiles(profileRows); const runtimeStates = await this.orchestrator.listRuntimeStates(rows.map((profile) => profile.profileName)); const runtimeMap = new Map(runtimeStates.map((state) => [state.profileName, state])); - const announcementMap = new Map(); const profileStatusMap = new Map(rows.map((row) => [row.profileName, row.status])); const now = this.now(); - for (const operation of recentResetOperations) { - if (announcementMap.has(operation.profileName)) continue; - if (!shouldExposeUpcomingReset(operation, profileStatusMap.get(operation.profileName))) continue; - const announcement = resolveUpcomingResetAnnouncement(operation, now); - if (announcement) announcementMap.set(operation.profileName, announcement); - } + const announcementMap = resolveUpcomingResetAnnouncements(recentResetOperations, profileStatusMap, now); return rows.map((row) => this.mapProfile(row, runtimeMap, announcementMap)); } @@ -198,6 +191,23 @@ export const shouldExposeUpcomingReset = ( operation.status === 'RUNNING' || (operation.status === 'SUCCEEDED' && profileStatus === 'RESERVED')); +export const resolveUpcomingResetAnnouncements = ( + operations: GatewayOperationRecord[], + profileStatusMap: ReadonlyMap, + now: Date +): Map => { + const announcements = new Map(); + const resolvedProfiles = new Set(); + for (const operation of operations) { + if (resolvedProfiles.has(operation.profileName)) continue; + resolvedProfiles.add(operation.profileName); + if (!shouldExposeUpcomingReset(operation, profileStatusMap.get(operation.profileName))) continue; + const announcement = resolveUpcomingResetAnnouncement(operation, now); + if (announcement) announcements.set(operation.profileName, announcement); + } + return announcements; +}; + export const resolveUpcomingResetAnnouncement = ( operation: GatewayOperationRecord, now: Date diff --git a/app/gateway-api/test/profileStatusService.test.ts b/app/gateway-api/test/profileStatusService.test.ts index c79deaa1..996ff735 100644 --- a/app/gateway-api/test/profileStatusService.test.ts +++ b/app/gateway-api/test/profileStatusService.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { resolveUpcomingResetAnnouncement, shouldExposeUpcomingReset } from '../src/lobby/profileStatusService.js'; +import { + resolveUpcomingResetAnnouncement, + resolveUpcomingResetAnnouncements, + shouldExposeUpcomingReset, +} from '../src/lobby/profileStatusService.js'; import type { GatewayOperationRecord } from '../src/orchestrator/profileRepository.js'; const buildOperation = (status: GatewayOperationRecord['status'] = 'QUEUED'): GatewayOperationRecord => ({ @@ -101,4 +105,48 @@ describe('resolveUpcomingResetAnnouncement', () => { incomplete.payload = { publicAnnouncement: { enabled: true, scenarioTitle: '황건적의 난' } }; expect(resolveUpcomingResetAnnouncement(incomplete, new Date('2026-08-27T04:00:00.000Z'))).toBeNull(); }); + + it('does not restore an older published notice after a newer reset replaces it', () => { + const olderPublished = buildOperation('SUCCEEDED'); + const newerImmediate = { + ...buildOperation('SUCCEEDED'), + id: '22222222-2222-4222-8222-222222222222', + payload: { + install: { + scenarioId: 1011, + preopenAt: '2026-09-01T05:30:00.000Z', + openAt: '2026-09-01T11:00:00.000Z', + }, + }, + scheduledAt: undefined, + createdAt: '2026-08-27T01:00:00.000Z', + updatedAt: '2026-08-27T01:00:00.000Z', + } satisfies GatewayOperationRecord; + + const result = resolveUpcomingResetAnnouncements( + [newerImmediate, olderPublished], + new Map([['che:2', 'RESERVED']]), + new Date('2026-08-27T04:00:00.000Z') + ); + + expect(result.has('che:2')).toBe(false); + }); + + it('treats a newer cancelled reset as the announcement boundary', () => { + const olderPublished = buildOperation('SUCCEEDED'); + const newerCancelled = { + ...buildOperation('CANCELLED'), + id: '33333333-3333-4333-8333-333333333333', + createdAt: '2026-08-27T01:00:00.000Z', + updatedAt: '2026-08-27T01:00:00.000Z', + } satisfies GatewayOperationRecord; + + const result = resolveUpcomingResetAnnouncements( + [newerCancelled, olderPublished], + new Map([['che:2', 'RESERVED']]), + new Date('2026-08-27T04:00:00.000Z') + ); + + expect(result.has('che:2')).toBe(false); + }); });