diff --git a/src/index.ts b/src/index.ts index 1a74e42..a0ae2bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,7 @@ import { buildRestartAnnouncement, buildInterruptedRestartAnnouncement, consumeRestartContext, + getRecoverableInterruptedGroups, getInterruptedRecoveryCandidates, inferRecentRestartContext, type RestartContext, @@ -196,7 +197,9 @@ async function announceRestartRecovery( 'Sent explicit restart recovery announcement', ); - for (const interrupted of explicitContext.interruptedGroups ?? []) { + for (const interrupted of getRecoverableInterruptedGroups( + explicitContext, + )) { if (interrupted.chatJid === explicitContext.chatJid) continue; if (hasRecentRestartAnnouncement(interrupted.chatJid, dedupeSince)) { continue; @@ -265,8 +268,8 @@ async function main(): Promise { ( status, ): status is typeof status & { - status: 'processing' | 'waiting'; - } => status.status !== 'inactive', + status: 'processing'; + } => status.status === 'processing', ) .map((status) => ({ chatJid: status.jid, diff --git a/src/restart-context.test.ts b/src/restart-context.test.ts index 2337f44..cfbde03 100644 --- a/src/restart-context.test.ts +++ b/src/restart-context.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; import { + buildRestartAnnouncement, + getRecoverableInterruptedGroups, getInterruptedRecoveryCandidates, type RestartContext, } from './restart-context.js'; @@ -16,8 +18,8 @@ function makeGroup(folder: string): RegisteredGroup { }; } -describe('getInterruptedRecoveryCandidates', () => { - it('returns only registered interrupted groups and deduplicates by chatJid', () => { +describe('restart recovery context', () => { + it('returns only registered processing groups and deduplicates by chatJid', () => { const roomBindings: Record = { 'dc:1': makeGroup('group-one'), 'dc:2': makeGroup('group-two'), @@ -38,8 +40,8 @@ describe('getInterruptedRecoveryCandidates', () => { pendingTasks: 0, }, { - chatJid: 'dc:1', - groupName: 'one-duplicate', + chatJid: 'dc:2', + groupName: 'two-waiting', status: 'waiting', elapsedMs: null, pendingMessages: false, @@ -47,7 +49,7 @@ describe('getInterruptedRecoveryCandidates', () => { }, { chatJid: 'dc:2', - groupName: 'two', + groupName: 'two-idle', status: 'idle', elapsedMs: null, pendingMessages: false, @@ -61,6 +63,14 @@ describe('getInterruptedRecoveryCandidates', () => { pendingMessages: true, pendingTasks: 0, }, + { + chatJid: 'dc:1', + groupName: 'one-duplicate', + status: 'processing', + elapsedMs: 2000, + pendingMessages: false, + pendingTasks: 1, + }, ], }; @@ -72,17 +82,40 @@ describe('getInterruptedRecoveryCandidates', () => { pendingMessages: true, pendingTasks: 0, }, - { - chatJid: 'dc:2', - groupFolder: 'group-two', - status: 'idle', - pendingMessages: false, - pendingTasks: 0, - }, ]); }); it('returns empty when there is no explicit restart context', () => { expect(getInterruptedRecoveryCandidates(null, {})).toEqual([]); }); + + it('uses only processing groups for restart announcements', () => { + const context: RestartContext = { + chatJid: 'dc:main', + summary: 'restart', + verify: [], + writtenAt: '2026-05-21T00:00:00.000Z', + interruptedGroups: [ + { + chatJid: 'dc:processing', + groupName: 'processing', + status: 'processing', + elapsedMs: 1000, + pendingMessages: false, + pendingTasks: 0, + }, + { + chatJid: 'dc:waiting', + groupName: 'waiting', + status: 'waiting', + elapsedMs: null, + pendingMessages: true, + pendingTasks: 1, + }, + ], + }; + + expect(getRecoverableInterruptedGroups(context)).toHaveLength(1); + expect(buildRestartAnnouncement(context)).toContain('중단 작업 감지: 1개'); + }); }); diff --git a/src/restart-context.ts b/src/restart-context.ts index f2aaedc..b008d2c 100644 --- a/src/restart-context.ts +++ b/src/restart-context.ts @@ -72,6 +72,20 @@ function readRestartContextFile(filePath: string): RestartContext | null { return data; } +function isRecoverableInterruptedGroup( + group: RestartInterruptedGroup, +): boolean { + return group.status === 'processing'; +} + +export function getRecoverableInterruptedGroups( + context: Pick | null, +): RestartInterruptedGroup[] { + return (context?.interruptedGroups ?? []).filter( + isRecoverableInterruptedGroup, + ); +} + function getLatestDistBuildTime(): number | null { const distDir = path.join(process.cwd(), 'dist'); if (!fs.existsSync(distDir)) return null; @@ -145,7 +159,10 @@ export function writeShutdownRestartContext( signal: string, serviceIds: string[] = [SERVICE_ID], ): string[] { - if (interruptedGroups.length === 0) return []; + const recoverableInterruptedGroups = interruptedGroups.filter( + isRecoverableInterruptedGroup, + ); + if (recoverableInterruptedGroups.length === 0) return []; fs.mkdirSync(DATA_DIR, { recursive: true }); const mainChatJid = @@ -156,8 +173,8 @@ export function writeShutdownRestartContext( const filePath = getRestartContextPath(serviceId); const existing = readRestartContextFile(filePath); const mergedInterrupted = [ - ...(existing?.interruptedGroups ?? []), - ...interruptedGroups, + ...getRecoverableInterruptedGroups(existing), + ...recoverableInterruptedGroups, ].filter( (group, index, all) => all.findIndex((candidate) => candidate.chatJid === group.chatJid) === @@ -221,8 +238,9 @@ export function consumeRestartContext(): RestartContext | null { export function buildRestartAnnouncement(context: RestartContext): string { const lines = ['재시작 완료.', `- 변경: ${context.summary}`]; - if (context.interruptedGroups && context.interruptedGroups.length > 0) { - lines.push(`- 중단 작업 감지: ${context.interruptedGroups.length}개`); + const recoverableInterruptedGroups = getRecoverableInterruptedGroups(context); + if (recoverableInterruptedGroups.length > 0) { + lines.push(`- 중단 작업 감지: ${recoverableInterruptedGroups.length}개`); } if (context.verify.length > 0) { lines.push(`- 검증: ${context.verify.join(', ')}`); @@ -240,7 +258,7 @@ export function getInterruptedRecoveryCandidates( const seen = new Set(); const candidates: RestartRecoveryCandidate[] = []; - for (const interrupted of context.interruptedGroups) { + for (const interrupted of getRecoverableInterruptedGroups(context)) { if (seen.has(interrupted.chatJid)) continue; const group = roomBindings[interrupted.chatJid]; if (!group) continue;