diff --git a/src/message-agent-executor-paired.test.ts b/src/message-agent-executor-paired.test.ts new file mode 100644 index 0000000..c7006e2 --- /dev/null +++ b/src/message-agent-executor-paired.test.ts @@ -0,0 +1,112 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('./db.js', () => ({ + completePairedTurn: vi.fn(), + failPairedTurn: vi.fn(), + getLastHumanMessageSender: vi.fn(() => '216851709744513024'), + getLatestTurnNumber: vi.fn(() => 0), + getPairedTaskById: vi.fn(), + insertPairedTurnOutput: vi.fn(), + refreshPairedTaskExecutionLease: vi.fn(() => true), + releasePairedTaskExecutionLease: vi.fn(), +})); + +vi.mock('./paired-execution-context.js', () => ({ + completePairedExecutionContext: vi.fn(), +})); + +vi.mock('./paired-turn-run-ownership.js', () => ({ + resolvePairedTurnRunOwnership: vi.fn(() => ({ state: 'active' })), +})); + +vi.mock('./message-runtime-follow-up.js', () => ({ + enqueuePairedFollowUpAfterEvent: vi.fn(), +})); + +import type { AgentOutput } from './agent-runner.js'; +import * as db from './db.js'; +import { createPairedExecutionLifecycle } from './message-agent-executor-paired.js'; + +const log = { + info: vi.fn(), + warn: vi.fn(), +}; + +describe('createPairedExecutionLifecycle', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('does not emit a second public notification after arbiter ESCALATE', async () => { + const outputs: AgentOutput[] = []; + + vi.mocked(db.getPairedTaskById).mockReturnValue({ + id: 'paired-task-arbiter-escalated', + chat_jid: 'group@test', + group_folder: 'test-group', + owner_service_id: 'claude', + reviewer_service_id: 'codex-main', + title: null, + source_ref: 'HEAD', + plan_notes: null, + round_trip_count: 1, + review_requested_at: '2026-04-09T00:00:00.000Z', + status: 'completed', + arbiter_verdict: 'escalate', + arbiter_requested_at: '2026-04-09T00:00:00.000Z', + completion_reason: 'arbiter_escalated', + created_at: '2026-04-09T00:00:00.000Z', + updated_at: '2026-04-09T00:00:01.000Z', + }); + + const lifecycle = createPairedExecutionLifecycle({ + pairedExecutionContext: { + task: { + id: 'paired-task-arbiter-escalated', + chat_jid: 'group@test', + group_folder: 'test-group', + owner_service_id: 'claude', + reviewer_service_id: 'codex-main', + title: null, + source_ref: 'HEAD', + plan_notes: null, + round_trip_count: 1, + review_requested_at: '2026-04-09T00:00:00.000Z', + status: 'in_arbitration', + arbiter_verdict: null, + arbiter_requested_at: '2026-04-09T00:00:00.000Z', + completion_reason: null, + created_at: '2026-04-09T00:00:00.000Z', + updated_at: '2026-04-09T00:00:00.000Z', + }, + workspace: null, + envOverrides: {}, + }, + pairedTurnIdentity: { + turnId: + 'paired-task-arbiter-escalated:2026-04-09T00:00:00.000Z:arbiter-turn', + taskId: 'paired-task-arbiter-escalated', + taskUpdatedAt: '2026-04-09T00:00:00.000Z', + intentKind: 'arbiter-turn', + role: 'arbiter', + }, + completedRole: 'arbiter', + chatJid: 'group@test', + runId: 'run-arbiter-escalated', + enqueueMessageCheck: vi.fn(), + onOutput: async (output) => { + outputs.push(output); + }, + log, + }); + + lifecycle.recordFinalOutputBeforeDelivery( + 'ESCALATE\nuser decision required', + ); + lifecycle.markStatus('succeeded'); + lifecycle.markSawOutput(true); + await lifecycle.asyncFinalize(); + + expect(outputs).toEqual([]); + }); +}); diff --git a/src/message-agent-executor-paired.ts b/src/message-agent-executor-paired.ts index eb76773..0eb9732 100644 --- a/src/message-agent-executor-paired.ts +++ b/src/message-agent-executor-paired.ts @@ -417,7 +417,6 @@ export function createPairedExecutionLifecycle(args: { const mention = sender ? `<@${sender}>` : ''; const notifications: Record = { escalated: `${mention} ⚠️ 자동 해결 불가 — 확인이 필요합니다.`, - arbiter_escalated: `${mention} ⚠️ 중재자 판단: 사람 개입이 필요합니다.`, }; const message = notifications[finishedTask.completion_reason]; if (message) {