fix: keep CI watcher completion actionable
This commit is contained in:
@@ -64,7 +64,10 @@ describe('GitHub watcher delivery identity', () => {
|
|||||||
|
|
||||||
const task = getTaskById('task-github-codex-paired-complete');
|
const task = getTaskById('task-github-codex-paired-complete');
|
||||||
expect(task).toBeDefined();
|
expect(task).toBeDefined();
|
||||||
const deps = { sendMessage: vi.fn(async () => {}) } as any;
|
const deps = {
|
||||||
|
sendMessage: vi.fn(async () => {}),
|
||||||
|
queue: { enqueueMessageCheck: vi.fn() },
|
||||||
|
} as any;
|
||||||
|
|
||||||
await runGithubCiTask(task!, deps);
|
await runGithubCiTask(task!, deps);
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ import type { ScheduledTask } from './types.js';
|
|||||||
|
|
||||||
function buildSchedulerDeps(overrides: {
|
function buildSchedulerDeps(overrides: {
|
||||||
sendMessage?: SchedulerDependencies['sendMessage'];
|
sendMessage?: SchedulerDependencies['sendMessage'];
|
||||||
|
sendTrackedMessage?: SchedulerDependencies['sendTrackedMessage'];
|
||||||
|
editTrackedMessage?: SchedulerDependencies['editTrackedMessage'];
|
||||||
enqueueMessageCheck?: (chatJid: string, ipcDir?: string) => void;
|
enqueueMessageCheck?: (chatJid: string, ipcDir?: string) => void;
|
||||||
}): SchedulerDependencies {
|
}): SchedulerDependencies {
|
||||||
return {
|
return {
|
||||||
@@ -55,6 +57,8 @@ function buildSchedulerDeps(overrides: {
|
|||||||
} as any,
|
} as any,
|
||||||
onProcess: () => {},
|
onProcess: () => {},
|
||||||
sendMessage: overrides.sendMessage ?? vi.fn(async () => {}),
|
sendMessage: overrides.sendMessage ?? vi.fn(async () => {}),
|
||||||
|
sendTrackedMessage: overrides.sendTrackedMessage,
|
||||||
|
editTrackedMessage: overrides.editTrackedMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +94,8 @@ function createReviewReadyPairedTask(now: string): void {
|
|||||||
function createGithubWatcherTask(overrides: {
|
function createGithubWatcherTask(overrides: {
|
||||||
id?: string;
|
id?: string;
|
||||||
roomRole?: ScheduledTask['room_role'];
|
roomRole?: ScheduledTask['room_role'];
|
||||||
|
statusMessageId?: string | null;
|
||||||
|
statusStartedAt?: string | null;
|
||||||
now: string;
|
now: string;
|
||||||
}): ScheduledTask {
|
}): ScheduledTask {
|
||||||
const taskId = overrides.id ?? 'task-github-owner-complete';
|
const taskId = overrides.id ?? 'task-github-owner-complete';
|
||||||
@@ -118,6 +124,8 @@ Managed by host-driven watcher.
|
|||||||
context_mode: 'isolated',
|
context_mode: 'isolated',
|
||||||
next_run: overrides.now,
|
next_run: overrides.now,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
|
status_message_id: overrides.statusMessageId,
|
||||||
|
status_started_at: overrides.statusStartedAt,
|
||||||
created_at: overrides.now,
|
created_at: overrides.now,
|
||||||
});
|
});
|
||||||
return getTaskById(taskId)!;
|
return getTaskById(taskId)!;
|
||||||
@@ -164,4 +172,100 @@ describe('GitHub CI watcher scheduler', () => {
|
|||||||
});
|
});
|
||||||
expect(getTaskById(task.id)).toBeUndefined();
|
expect(getTaskById(task.id)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('queues an owner turn even when the paired task already closed before CI completion', 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';
|
||||||
|
const task = createGithubWatcherTask({ now, roomRole: 'owner' });
|
||||||
|
const enqueueMessageCheck = vi.fn();
|
||||||
|
|
||||||
|
await runGithubCiTask(task, buildSchedulerDeps({ enqueueMessageCheck }));
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not queue owner wake-up for non-owner CI watchers', 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: 'reviewer' });
|
||||||
|
const enqueueMessageCheck = vi.fn();
|
||||||
|
|
||||||
|
await runGithubCiTask(task, buildSchedulerDeps({ enqueueMessageCheck }));
|
||||||
|
|
||||||
|
expect(enqueueMessageCheck).not.toHaveBeenCalled();
|
||||||
|
expect(getRecentChatMessages('shared@g.us', 5)).toEqual([]);
|
||||||
|
expect(getTaskById(task.id)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('edits the tracked watcher status and still wakes owner without a duplicate bot completion message', 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',
|
||||||
|
statusMessageId: 'status-msg-1',
|
||||||
|
statusStartedAt: now,
|
||||||
|
});
|
||||||
|
const sendMessage = vi.fn(async () => {});
|
||||||
|
const sendTrackedMessage = vi.fn(async () => 'status-msg-new');
|
||||||
|
const editTrackedMessage = vi.fn(async () => {});
|
||||||
|
const enqueueMessageCheck = vi.fn();
|
||||||
|
|
||||||
|
await runGithubCiTask(
|
||||||
|
task,
|
||||||
|
buildSchedulerDeps({
|
||||||
|
sendMessage,
|
||||||
|
sendTrackedMessage,
|
||||||
|
editTrackedMessage,
|
||||||
|
enqueueMessageCheck,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(editTrackedMessage).toHaveBeenCalledWith(
|
||||||
|
'shared@g.us',
|
||||||
|
'status-msg-1',
|
||||||
|
expect.stringContaining('CI 감시 종료: GitHub Actions run 777777'),
|
||||||
|
);
|
||||||
|
expect(sendTrackedMessage).not.toHaveBeenCalled();
|
||||||
|
expect(sendMessage).not.toHaveBeenCalled();
|
||||||
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { getErrorMessage } from './utils.js';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
deleteTask,
|
deleteTask,
|
||||||
getLatestOpenPairedTaskForChat,
|
|
||||||
getTaskById,
|
getTaskById,
|
||||||
logTaskRun,
|
logTaskRun,
|
||||||
storeChatMetadata,
|
storeChatMetadata,
|
||||||
@@ -34,11 +33,6 @@ function enqueueOwnerAfterTerminalCiWatcher(args: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pairedTask = getLatestOpenPairedTaskForChat(args.task.chat_jid);
|
|
||||||
if (!pairedTask) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
const content = [
|
const content = [
|
||||||
'[CI watcher completed]',
|
'[CI watcher completed]',
|
||||||
@@ -46,7 +40,7 @@ function enqueueOwnerAfterTerminalCiWatcher(args: {
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
storeChatMetadata(args.task.chat_jid, timestamp, undefined, 'discord', true);
|
storeChatMetadata(args.task.chat_jid, timestamp, undefined, 'discord', true);
|
||||||
storeMessage({
|
storeMessage({
|
||||||
id: `watch-ci-completed:${args.task.id}:${Date.now()}`,
|
id: `watch-ci-completed:${args.task.id}`,
|
||||||
chat_jid: args.task.chat_jid,
|
chat_jid: args.task.chat_jid,
|
||||||
sender: 'ci-watcher',
|
sender: 'ci-watcher',
|
||||||
sender_name: 'CI watcher',
|
sender_name: 'CI watcher',
|
||||||
@@ -65,8 +59,6 @@ function enqueueOwnerAfterTerminalCiWatcher(args: {
|
|||||||
taskId: args.task.id,
|
taskId: args.task.id,
|
||||||
chatJid: args.task.chat_jid,
|
chatJid: args.task.chat_jid,
|
||||||
groupFolder: args.task.group_folder,
|
groupFolder: args.task.group_folder,
|
||||||
pairedTaskId: pairedTask.id,
|
|
||||||
pairedTaskStatus: pairedTask.status,
|
|
||||||
},
|
},
|
||||||
'Queued owner follow-up after terminal CI watcher completion',
|
'Queued owner follow-up after terminal CI watcher completion',
|
||||||
);
|
);
|
||||||
@@ -107,7 +99,7 @@ export async function runGithubCiTask(
|
|||||||
|
|
||||||
if (check.terminal) {
|
if (check.terminal) {
|
||||||
await statusTracker.update('completed');
|
await statusTracker.update('completed');
|
||||||
if (check.completionMessage) {
|
if (!statusTracker.enabled && check.completionMessage) {
|
||||||
await sendScheduledMessage(
|
await sendScheduledMessage(
|
||||||
deps,
|
deps,
|
||||||
task.chat_jid,
|
task.chat_jid,
|
||||||
|
|||||||
@@ -1741,7 +1741,7 @@ Check the run.
|
|||||||
expect(updatedTask?.status_started_at).toBe('2026-03-19T07:00:00.000Z');
|
expect(updatedTask?.status_started_at).toBe('2026-03-19T07:00:00.000Z');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends a fresh watcher status message when the watcher completes', async () => {
|
it('edits the existing watcher status message when the watcher completes', async () => {
|
||||||
vi.setSystemTime(new Date('2026-03-19T07:05:00.000Z'));
|
vi.setSystemTime(new Date('2026-03-19T07:05:00.000Z'));
|
||||||
createTask({
|
createTask({
|
||||||
id: 'task-watch-status-completed',
|
id: 'task-watch-status-completed',
|
||||||
@@ -1780,18 +1780,20 @@ Check the run.
|
|||||||
|
|
||||||
await tracker.update('completed');
|
await tracker.update('completed');
|
||||||
|
|
||||||
expect(editTrackedMessage).not.toHaveBeenCalled();
|
expect(editTrackedMessage).toHaveBeenCalledWith(
|
||||||
expect(sendTrackedMessage).toHaveBeenCalledWith(
|
|
||||||
'shared@g.us',
|
'shared@g.us',
|
||||||
|
'msg-old',
|
||||||
expect.stringContaining(`${TASK_STATUS_MESSAGE_PREFIX}CI 감시 종료:`),
|
expect.stringContaining(`${TASK_STATUS_MESSAGE_PREFIX}CI 감시 종료:`),
|
||||||
);
|
);
|
||||||
expect(sendTrackedMessage).toHaveBeenCalledWith(
|
expect(editTrackedMessage).toHaveBeenCalledWith(
|
||||||
'shared@g.us',
|
'shared@g.us',
|
||||||
|
'msg-old',
|
||||||
expect.stringContaining('- 상태: 완료'),
|
expect.stringContaining('- 상태: 완료'),
|
||||||
);
|
);
|
||||||
|
expect(sendTrackedMessage).not.toHaveBeenCalled();
|
||||||
|
|
||||||
const updatedTask = getTaskById('task-watch-status-completed');
|
const updatedTask = getTaskById('task-watch-status-completed');
|
||||||
expect(updatedTask?.status_message_id).toBe('msg-terminal');
|
expect(updatedTask?.status_message_id).toBe('msg-old');
|
||||||
expect(updatedTask?.status_started_at).toBe('2026-03-19T07:00:00.000Z');
|
expect(updatedTask?.status_started_at).toBe('2026-03-19T07:00:00.000Z');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ export function createTaskStatusTracker(
|
|||||||
nextRun,
|
nextRun,
|
||||||
})}`;
|
})}`;
|
||||||
|
|
||||||
const sendFreshTerminalStatus = phase === 'completed';
|
if (statusMessageId) {
|
||||||
if (statusMessageId && !sendFreshTerminalStatus) {
|
|
||||||
try {
|
try {
|
||||||
await transport.editTrackedMessage!(
|
await transport.editTrackedMessage!(
|
||||||
task.chat_jid,
|
task.chat_jid,
|
||||||
@@ -82,18 +81,6 @@ export function createTaskStatusTracker(
|
|||||||
statusMessageId = null;
|
statusMessageId = null;
|
||||||
persist();
|
persist();
|
||||||
}
|
}
|
||||||
} else if (statusMessageId && sendFreshTerminalStatus) {
|
|
||||||
logger.debug(
|
|
||||||
{
|
|
||||||
taskId: task.id,
|
|
||||||
chatJid: task.chat_jid,
|
|
||||||
statusMessageId,
|
|
||||||
phase,
|
|
||||||
},
|
|
||||||
'Sending terminal watcher status as a new message instead of editing the existing status message',
|
|
||||||
);
|
|
||||||
statusMessageId = null;
|
|
||||||
persist();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextMessageId = await transport.sendTrackedMessage!(
|
const nextMessageId = await transport.sendTrackedMessage!(
|
||||||
|
|||||||
Reference in New Issue
Block a user