fix: wake owner after CI watcher completes

This commit is contained in:
ejclaw
2026-05-27 05:24:42 +09:00
parent 39c4637384
commit 5a0ad0a1b3
2 changed files with 223 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { checkGitHubActionsRunMock } = vi.hoisted(() => ({
checkGitHubActionsRunMock: vi.fn(),
}));
vi.mock('./github-ci.js', () => ({
checkGitHubActionsRun: checkGitHubActionsRunMock,
computeGitHubWatcherDelayMs: vi.fn(() => 15_000),
MAX_GITHUB_CONSECUTIVE_ERRORS: 5,
parseGitHubCiMetadata: vi.fn((raw: string | null | undefined) => {
if (!raw) return null;
return JSON.parse(raw);
}),
serializeGitHubCiMetadata: vi.fn((metadata: unknown) =>
JSON.stringify(metadata),
),
}));
vi.mock('./logger.js', () => {
const mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
child: (_bindings?: Record<string, unknown>) => mockLogger,
};
return {
logger: mockLogger,
createScopedLogger: (_bindings?: Record<string, unknown>) => mockLogger,
};
});
import {
_initTestDatabase,
createPairedTask,
createTask,
getRecentChatMessages,
getTaskById,
} from './db.js';
import { runGithubCiTask } from './task-scheduler-github.js';
import type { SchedulerDependencies } from './task-scheduler-types.js';
import type { ScheduledTask } from './types.js';
function buildSchedulerDeps(overrides: {
sendMessage?: SchedulerDependencies['sendMessage'];
enqueueMessageCheck?: (chatJid: string, ipcDir?: string) => void;
}): SchedulerDependencies {
return {
serviceAgentType: 'codex',
roomBindings: () => ({}),
getSessions: () => ({}),
queue: {
enqueueMessageCheck: overrides.enqueueMessageCheck ?? vi.fn(),
} as any,
onProcess: () => {},
sendMessage: overrides.sendMessage ?? vi.fn(async () => {}),
};
}
function createReviewReadyPairedTask(now: string): void {
createPairedTask({
id: 'paired-review-ready',
chat_jid: 'shared@g.us',
group_folder: 'shared-group',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude-review',
owner_agent_type: 'codex',
reviewer_agent_type: 'claude-code',
arbiter_agent_type: 'codex',
title: null,
source_ref: null,
plan_notes: null,
review_requested_at: null,
round_trip_count: 1,
owner_failure_count: 0,
owner_step_done_streak: 0,
finalize_step_done_count: 0,
task_done_then_user_reopen_count: 0,
empty_step_done_streak: 0,
status: 'review_ready',
arbiter_verdict: null,
arbiter_requested_at: null,
completion_reason: null,
created_at: now,
updated_at: now,
});
}
function createGithubWatcherTask(overrides: {
id?: string;
roomRole?: ScheduledTask['room_role'];
now: string;
}): ScheduledTask {
const taskId = overrides.id ?? 'task-github-owner-complete';
createTask({
id: taskId,
group_folder: 'shared-group',
chat_jid: 'shared@g.us',
agent_type: 'codex',
room_role: overrides.roomRole,
ci_provider: 'github',
ci_metadata: JSON.stringify({
repo: 'owner/repo',
run_id: 777777,
}),
prompt: `
[BACKGROUND CI WATCH]
Watch target:
GitHub Actions run 777777
Check instructions:
Managed by host-driven watcher.
`.trim(),
schedule_type: 'interval',
schedule_value: '15000',
context_mode: 'isolated',
next_run: overrides.now,
status: 'active',
created_at: overrides.now,
});
return getTaskById(taskId)!;
}
describe('GitHub CI watcher scheduler', () => {
beforeEach(() => {
_initTestDatabase();
checkGitHubActionsRunMock.mockReset();
});
it('queues an owner turn after an owner CI watcher completes in a paired room', async () => {
checkGitHubActionsRunMock.mockResolvedValueOnce({
terminal: true,
resultSummary: '성공: owner/repo run 777777',
completionMessage: 'CI 완료: GitHub Actions run 777777\n판정: 성공',
});
const now = '2026-02-22T00:00:00.000Z';
createReviewReadyPairedTask(now);
const task = createGithubWatcherTask({ now, roomRole: 'owner' });
const sendMessage = vi.fn(async () => {});
const enqueueMessageCheck = vi.fn();
await runGithubCiTask(
task,
buildSchedulerDeps({ sendMessage, enqueueMessageCheck }),
);
expect(sendMessage).toHaveBeenCalledWith(
'shared@g.us',
'CI 완료: GitHub Actions run 777777\n판정: 성공',
);
expect(enqueueMessageCheck).toHaveBeenCalledWith(
'shared@g.us',
expect.stringContaining('shared-group'),
);
expect(getRecentChatMessages('shared@g.us', 5).at(-1)).toMatchObject({
sender: 'ci-watcher',
sender_name: 'CI watcher',
is_bot_message: false,
message_source_kind: 'trusted_external_bot',
content:
'[CI watcher completed]\nCI 완료: GitHub Actions run 777777\n판정: 성공',
});
expect(getTaskById(task.id)).toBeUndefined();
});
});

View File

@@ -2,11 +2,15 @@ import { getErrorMessage } from './utils.js';
import {
deleteTask,
getLatestOpenPairedTaskForChat,
getTaskById,
logTaskRun,
storeChatMetadata,
storeMessage,
updateTask,
updateTaskAfterRun,
} from './db.js';
import { resolveGroupIpcPath } from './group-folder.js';
import { logger } from './logger.js';
import { createTaskStatusTracker } from './task-status-tracker.js';
import { extractWatchCiTarget } from './task-watch-status.js';
@@ -21,6 +25,53 @@ import { sendScheduledMessage } from './task-scheduler-runtime.js';
import type { SchedulerDependencies } from './task-scheduler-types.js';
import type { ScheduledTask } from './types.js';
function enqueueOwnerAfterTerminalCiWatcher(args: {
task: ScheduledTask;
deps: SchedulerDependencies;
completionText: string | null | undefined;
}): void {
if (args.task.room_role !== 'owner') {
return;
}
const pairedTask = getLatestOpenPairedTaskForChat(args.task.chat_jid);
if (!pairedTask) {
return;
}
const timestamp = new Date().toISOString();
const content = [
'[CI watcher completed]',
args.completionText?.trim() || 'CI watcher completed.',
].join('\n');
storeChatMetadata(args.task.chat_jid, timestamp, undefined, 'discord', true);
storeMessage({
id: `watch-ci-completed:${args.task.id}:${Date.now()}`,
chat_jid: args.task.chat_jid,
sender: 'ci-watcher',
sender_name: 'CI watcher',
content,
timestamp,
is_from_me: false,
is_bot_message: false,
message_source_kind: 'trusted_external_bot',
});
args.deps.queue.enqueueMessageCheck(
args.task.chat_jid,
resolveGroupIpcPath(args.task.group_folder),
);
logger.info(
{
taskId: args.task.id,
chatJid: args.task.chat_jid,
groupFolder: args.task.group_folder,
pairedTaskId: pairedTask.id,
pairedTaskStatus: pairedTask.status,
},
'Queued owner follow-up after terminal CI watcher completion',
);
}
export async function runGithubCiTask(
task: ScheduledTask,
deps: SchedulerDependencies,
@@ -66,6 +117,11 @@ export async function runGithubCiTask(
}
deleteTask(task.id);
completedAndDeleted = true;
enqueueOwnerAfterTerminalCiWatcher({
task,
deps,
completionText: check.completionMessage ?? result,
});
logger.info(
{
taskId: task.id,