167 lines
4.6 KiB
TypeScript
167 lines
4.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
_initTestDatabase,
|
|
createPairedTask,
|
|
failPairedTurn,
|
|
markPairedTurnRunning,
|
|
} from './db.js';
|
|
import { CODEX_MAIN_SERVICE_ID } from './config.js';
|
|
import {
|
|
getCodexBadRequestRepeatThreshold,
|
|
notifyOwnerCodexBadRequestObservation,
|
|
} from './session-auto-healer.js';
|
|
import type { PairedTask } from './types.js';
|
|
|
|
const BAD_REQUEST_ERROR = '{"detail":"Bad Request"}';
|
|
const originalThresholdEnv = process.env.CODEX_BAD_REQUEST_REPEAT_THRESHOLD;
|
|
|
|
const pairedTurnIdentity = {
|
|
turnId: 'task-1:updated:owner-follow-up',
|
|
taskId: 'task-1',
|
|
taskUpdatedAt: '2026-04-29T01:00:00.000Z',
|
|
intentKind: 'owner-follow-up' as const,
|
|
role: 'owner' as const,
|
|
};
|
|
|
|
function makeTask(overrides: Partial<PairedTask> = {}): PairedTask {
|
|
return {
|
|
id: 'task-1',
|
|
chat_jid: 'dc:room',
|
|
group_folder: 'eyejokerdb-9',
|
|
owner_service_id: CODEX_MAIN_SERVICE_ID,
|
|
reviewer_service_id: 'claude',
|
|
title: null,
|
|
source_ref: null,
|
|
plan_notes: null,
|
|
review_requested_at: null,
|
|
round_trip_count: 1,
|
|
status: 'active',
|
|
arbiter_verdict: null,
|
|
arbiter_requested_at: null,
|
|
completion_reason: null,
|
|
created_at: '2026-04-29T08:00:00.000Z',
|
|
updated_at: pairedTurnIdentity.taskUpdatedAt,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function recordOwnerCodexFailure(runId: string): void {
|
|
markPairedTurnRunning({
|
|
turnIdentity: pairedTurnIdentity,
|
|
executorServiceId: CODEX_MAIN_SERVICE_ID,
|
|
executorAgentType: 'codex',
|
|
runId,
|
|
});
|
|
failPairedTurn({
|
|
turnIdentity: pairedTurnIdentity,
|
|
error: BAD_REQUEST_ERROR,
|
|
});
|
|
}
|
|
|
|
describe('session-auto-healer', () => {
|
|
beforeEach(() => {
|
|
_initTestDatabase();
|
|
delete process.env.CODEX_BAD_REQUEST_REPEAT_THRESHOLD;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalThresholdEnv === undefined) {
|
|
delete process.env.CODEX_BAD_REQUEST_REPEAT_THRESHOLD;
|
|
} else {
|
|
process.env.CODEX_BAD_REQUEST_REPEAT_THRESHOLD = originalThresholdEnv;
|
|
}
|
|
});
|
|
|
|
it('uses a conservative default Bad Request repeat threshold', () => {
|
|
expect(getCodexBadRequestRepeatThreshold()).toBe(3);
|
|
});
|
|
|
|
it('supports threshold override for observation tuning', () => {
|
|
process.env.CODEX_BAD_REQUEST_REPEAT_THRESHOLD = '2';
|
|
|
|
expect(getCodexBadRequestRepeatThreshold()).toBe(2);
|
|
});
|
|
|
|
it('ignores invalid threshold overrides', () => {
|
|
process.env.CODEX_BAD_REQUEST_REPEAT_THRESHOLD = '1';
|
|
|
|
expect(getCodexBadRequestRepeatThreshold()).toBe(3);
|
|
});
|
|
|
|
it('notifies once when owner Codex reaches the Bad Request observation threshold', async () => {
|
|
createPairedTask(makeTask());
|
|
for (const runId of ['run-1', 'run-2', 'run-3']) {
|
|
recordOwnerCodexFailure(runId);
|
|
}
|
|
const channel = { sendMessage: vi.fn() };
|
|
|
|
const notified = await notifyOwnerCodexBadRequestObservation({
|
|
chatJid: 'dc:room',
|
|
runId: 'run-1',
|
|
groupFolder: 'eyejokerdb-9',
|
|
channel,
|
|
outputStatus: 'error',
|
|
visiblePhase: 'silent',
|
|
deliveryRole: 'owner',
|
|
agentType: 'codex',
|
|
pairedTurnIdentity,
|
|
threshold: 3,
|
|
});
|
|
|
|
expect(notified).toBe(true);
|
|
expect(channel.sendMessage).toHaveBeenCalledWith(
|
|
'dc:room',
|
|
expect.stringContaining('자동복구는 아직 비활성화'),
|
|
);
|
|
expect(channel.sendMessage).toHaveBeenCalledWith(
|
|
'dc:room',
|
|
expect.stringContaining('task=task-1'),
|
|
);
|
|
});
|
|
|
|
it('does not notify when the failure had visible output', async () => {
|
|
const channel = { sendMessage: vi.fn() };
|
|
|
|
const notified = await notifyOwnerCodexBadRequestObservation({
|
|
chatJid: 'dc:room',
|
|
runId: 'run-1',
|
|
groupFolder: 'eyejokerdb-9',
|
|
channel,
|
|
outputStatus: 'error',
|
|
visiblePhase: 'progress',
|
|
deliveryRole: 'owner',
|
|
agentType: 'codex',
|
|
pairedTurnIdentity,
|
|
threshold: 3,
|
|
});
|
|
|
|
expect(notified).toBe(false);
|
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not notify again after the exact threshold has already passed', async () => {
|
|
createPairedTask(makeTask());
|
|
for (const runId of ['run-1', 'run-2', 'run-3', 'run-4']) {
|
|
recordOwnerCodexFailure(runId);
|
|
}
|
|
const channel = { sendMessage: vi.fn() };
|
|
|
|
const notified = await notifyOwnerCodexBadRequestObservation({
|
|
chatJid: 'dc:room',
|
|
runId: 'run-1',
|
|
groupFolder: 'eyejokerdb-9',
|
|
channel,
|
|
outputStatus: 'error',
|
|
visiblePhase: 'silent',
|
|
deliveryRole: 'owner',
|
|
agentType: 'codex',
|
|
pairedTurnIdentity,
|
|
threshold: 3,
|
|
});
|
|
|
|
expect(notified).toBe(false);
|
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
|
});
|
|
});
|