From 95abda197d2b25eeca4c38b757ec7c2b532c496d Mon Sep 17 00:00:00 2001 From: ejclaw Date: Thu, 30 Apr 2026 00:58:39 +0900 Subject: [PATCH] cover paired owner progress entry path --- src/message-runtime-progress-entry.test.ts | 144 +++++++++++++++++++++ src/message-runtime.ts | 2 +- 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/message-runtime-progress-entry.test.ts diff --git a/src/message-runtime-progress-entry.test.ts b/src/message-runtime-progress-entry.test.ts new file mode 100644 index 0000000..73c77eb --- /dev/null +++ b/src/message-runtime-progress-entry.test.ts @@ -0,0 +1,144 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const runtimeMocks = vi.hoisted(() => ({ + executeTurn: vi.fn(async () => ({ + outputStatus: 'success' as const, + deliverySucceeded: true, + visiblePhase: 'progress' as const, + })), +})); + +vi.mock('./message-runtime-turns.js', () => ({ + createRunAgent: vi.fn(() => vi.fn()), + createExecuteTurn: vi.fn(() => runtimeMocks.executeTurn), + isDuplicateOfLastBotFinal: vi.fn(() => false), + labelPairedSenders: vi.fn((_channels, _chatJid, messages) => messages), +})); + +import { + _initTestDatabase, + _setRegisteredGroupForTests, + getLatestOpenPairedTaskForChat, + storeChatMetadata, + storeMessage, +} from './db.js'; +import { createMessageRuntime } from './message-runtime.js'; +import { + clearGlobalFailover, + refreshChannelOwnerCache, +} from './service-routing.js'; +import type { Channel, RegisteredGroup } from './types.js'; + +const chatJid = 'group@test'; +const timestamp = '2026-04-30T00:30:00.000Z'; + +function makeGroup(): RegisteredGroup { + return { + name: 'Test Group', + folder: 'test-group', + trigger: '@Andy', + added_at: timestamp, + requiresTrigger: false, + agentType: 'codex', + workDir: process.cwd(), + }; +} + +function makeChannel(name: string, ownsJid: boolean): Channel { + return { + name, + connect: vi.fn(), + sendMessage: vi.fn(), + isConnected: vi.fn(() => true), + ownsJid: vi.fn((jid: string) => ownsJid && jid === chatJid), + disconnect: vi.fn(), + setTyping: vi.fn(), + } as unknown as Channel; +} + +describe('message-runtime progress entry path', () => { + beforeEach(() => { + _initTestDatabase(); + refreshChannelOwnerCache(true); + clearGlobalFailover(); + runtimeMocks.executeTurn.mockClear(); + runtimeMocks.executeTurn.mockResolvedValue({ + outputStatus: 'success', + deliverySucceeded: true, + visiblePhase: 'progress', + }); + }); + + it('passes pairedTurnIdentity through the production owner-turn entry path when no task is open', async () => { + const group = makeGroup(); + const ownerChannel = makeChannel('discord', true); + const reviewerChannel = makeChannel('discord-review', false); + const lastAgentTimestamps: Record = {}; + const saveState = vi.fn(); + + _setRegisteredGroupForTests(chatJid, { + ...group, + agentType: 'claude-code', + }); + _setRegisteredGroupForTests(chatJid, group); + refreshChannelOwnerCache(true); + storeChatMetadata(chatJid, timestamp, 'Test Group'); + storeMessage({ + id: 'human-first-owner-turn', + chat_jid: chatJid, + sender: 'user@test', + sender_name: 'User', + content: '디스코드에는 progress가 보이는데 사이트에는 안 보여요', + timestamp, + is_bot_message: false, + is_from_me: false, + }); + + const runtime = createMessageRuntime({ + assistantName: 'Andy', + idleTimeout: 1_000, + pollInterval: 1_000, + timezone: 'UTC', + triggerPattern: /^@Andy\b/i, + channels: [ownerChannel, reviewerChannel], + queue: { + enqueueMessageCheck: vi.fn(), + registerProcess: vi.fn(), + closeStdin: vi.fn(), + notifyIdle: vi.fn(), + } as any, + getRoomBindings: () => ({ [chatJid]: group }), + getSessions: () => ({}), + getLastTimestamp: () => '', + setLastTimestamp: vi.fn(), + getLastAgentTimestamps: () => lastAgentTimestamps, + saveState, + persistSession: vi.fn(), + clearSession: vi.fn(), + }); + + const result = await runtime.processGroupMessages(chatJid, { + runId: 'run-first-owner-entry', + reason: 'messages', + }); + + const freshTask = getLatestOpenPairedTaskForChat(chatJid); + expect(result).toBe(true); + expect(freshTask).toBeDefined(); + expect(runtimeMocks.executeTurn).toHaveBeenCalledWith( + expect.objectContaining({ + deliveryRole: 'owner', + forcedRole: 'owner', + pairedTurnIdentity: { + turnId: `${freshTask!.id}:${freshTask!.updated_at}:owner-turn`, + taskId: freshTask!.id, + taskUpdatedAt: freshTask!.updated_at, + intentKind: 'owner-turn', + role: 'owner', + }, + }), + ); + expect(lastAgentTimestamps).toEqual({ [chatJid]: '1' }); + expect(saveState).toHaveBeenCalled(); + }); +}); diff --git a/src/message-runtime.ts b/src/message-runtime.ts index ec36db9..edcb830 100644 --- a/src/message-runtime.ts +++ b/src/message-runtime.ts @@ -490,7 +490,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { timezone: deps.timezone, missedMessages, task: hasReviewerLease(chatJid) - ? getLatestOpenPairedTaskForChat(chatJid) + ? (getLatestOpenPairedTaskForChat(chatJid) ?? null) : undefined, roleToChannel, ownerChannel: channel,