Audit outbound delivery and fix reviewer turn routing
This commit is contained in:
224
src/message-turn-controller.test.ts
Normal file
224
src/message-turn-controller.test.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
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 { MessageTurnController } from './message-turn-controller.js';
|
||||
import { logger } from './logger.js';
|
||||
import type { Channel, RegisteredGroup } from './types.js';
|
||||
import type { PairedTurnIdentity } from './paired-turn-identity.js';
|
||||
|
||||
function makeGroup(): RegisteredGroup {
|
||||
return {
|
||||
name: 'Test Group',
|
||||
folder: 'test-group',
|
||||
trigger: '@Andy',
|
||||
added_at: new Date().toISOString(),
|
||||
requiresTrigger: false,
|
||||
agentType: 'claude-code',
|
||||
};
|
||||
}
|
||||
|
||||
function makeChannel(): Channel {
|
||||
return {
|
||||
name: 'discord-review',
|
||||
connect: vi.fn().mockResolvedValue(undefined),
|
||||
disconnect: vi.fn().mockResolvedValue(undefined),
|
||||
isConnected: vi.fn(() => true),
|
||||
ownsJid: vi.fn(() => true),
|
||||
sendMessage: vi.fn().mockResolvedValue(undefined),
|
||||
sendAndTrack: vi.fn().mockResolvedValue('progress-1'),
|
||||
editMessage: vi.fn().mockResolvedValue(undefined),
|
||||
setTyping: vi.fn().mockResolvedValue(undefined),
|
||||
getOutboundAuditMeta: vi.fn(() => ({
|
||||
channelName: 'discord-review',
|
||||
botUserId: 'bot-review',
|
||||
botUsername: 'reviewer-bot',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function makeTurnIdentity(): PairedTurnIdentity {
|
||||
return {
|
||||
turnId: 'task-1:2026-04-10T14:22:00.000Z:reviewer-turn',
|
||||
taskId: 'task-1',
|
||||
taskUpdatedAt: '2026-04-10T14:22:00.000Z',
|
||||
intentKind: 'reviewer-turn',
|
||||
role: 'reviewer',
|
||||
};
|
||||
}
|
||||
|
||||
async function flushAsync(): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
function getAuditEntries(): Array<Record<string, unknown>> {
|
||||
return vi
|
||||
.mocked(logger.info)
|
||||
.mock.calls.map(([payload]) => payload)
|
||||
.filter(
|
||||
(payload): payload is Record<string, unknown> =>
|
||||
!!payload && typeof payload === 'object' && 'auditEvent' in payload,
|
||||
);
|
||||
}
|
||||
|
||||
describe('MessageTurnController outbound audit logging', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('logs outbound progress and final audit context with role/service/turn metadata', async () => {
|
||||
const channel = makeChannel();
|
||||
const deliverFinalText = vi.fn().mockResolvedValue(true);
|
||||
const controller = new MessageTurnController({
|
||||
chatJid: 'dc:test-room',
|
||||
group: makeGroup(),
|
||||
runId: 'run-review-1',
|
||||
channel,
|
||||
idleTimeout: 1_000,
|
||||
failureFinalText: '실패',
|
||||
isClaudeCodeAgent: true,
|
||||
clearSession: vi.fn(),
|
||||
requestClose: vi.fn(),
|
||||
deliverFinalText,
|
||||
deliveryRole: 'reviewer',
|
||||
deliveryServiceId: 'codex-review',
|
||||
pairedTurnIdentity: makeTurnIdentity(),
|
||||
});
|
||||
|
||||
await controller.start();
|
||||
await controller.handleOutput({
|
||||
status: 'success',
|
||||
phase: 'progress',
|
||||
result: '첫 진행 상황',
|
||||
} as any);
|
||||
await controller.handleOutput({
|
||||
status: 'success',
|
||||
phase: 'progress',
|
||||
result: '둘째 진행 상황',
|
||||
} as any);
|
||||
await flushAsync();
|
||||
await controller.handleOutput({
|
||||
status: 'success',
|
||||
phase: 'final',
|
||||
result: '최종 답변',
|
||||
} as any);
|
||||
await controller.finish('success');
|
||||
|
||||
expect(channel.sendAndTrack).toHaveBeenCalledTimes(1);
|
||||
expect(channel.editMessage).toHaveBeenCalledWith(
|
||||
'dc:test-room',
|
||||
'progress-1',
|
||||
expect.stringContaining('둘째 진행 상황'),
|
||||
);
|
||||
expect(deliverFinalText).toHaveBeenCalledWith('최종 답변');
|
||||
|
||||
expect(getAuditEntries()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
auditEvent: 'progress-create',
|
||||
chatJid: 'dc:test-room',
|
||||
runId: 'run-review-1',
|
||||
deliveryRole: 'reviewer',
|
||||
serviceId: 'codex-review',
|
||||
turnId: 'task-1:2026-04-10T14:22:00.000Z:reviewer-turn',
|
||||
turnRole: 'reviewer',
|
||||
channelName: 'discord-review',
|
||||
botUserId: 'bot-review',
|
||||
botUsername: 'reviewer-bot',
|
||||
messageId: 'progress-1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
auditEvent: 'progress-edit',
|
||||
deliveryRole: 'reviewer',
|
||||
serviceId: 'codex-review',
|
||||
turnId: 'task-1:2026-04-10T14:22:00.000Z:reviewer-turn',
|
||||
messageId: 'progress-1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
auditEvent: 'final-delivery-attempt',
|
||||
deliveryRole: 'reviewer',
|
||||
serviceId: 'codex-review',
|
||||
turnId: 'task-1:2026-04-10T14:22:00.000Z:reviewer-turn',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
auditEvent: 'final-delivery-result',
|
||||
deliveryRole: 'reviewer',
|
||||
serviceId: 'codex-review',
|
||||
turnId: 'task-1:2026-04-10T14:22:00.000Z:reviewer-turn',
|
||||
delivered: true,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('logs fallback progress audit when tracked message creation fails', async () => {
|
||||
const channel = makeChannel();
|
||||
const deliverFinalText = vi.fn().mockResolvedValue(true);
|
||||
vi.mocked(channel.sendAndTrack!).mockRejectedValueOnce(
|
||||
new Error('send failed'),
|
||||
);
|
||||
|
||||
const controller = new MessageTurnController({
|
||||
chatJid: 'dc:test-room',
|
||||
group: makeGroup(),
|
||||
runId: 'run-review-fallback',
|
||||
channel,
|
||||
idleTimeout: 1_000,
|
||||
failureFinalText: '실패',
|
||||
isClaudeCodeAgent: true,
|
||||
clearSession: vi.fn(),
|
||||
requestClose: vi.fn(),
|
||||
deliverFinalText,
|
||||
deliveryRole: 'reviewer',
|
||||
deliveryServiceId: 'codex-review',
|
||||
pairedTurnIdentity: makeTurnIdentity(),
|
||||
});
|
||||
|
||||
await controller.start();
|
||||
await controller.handleOutput({
|
||||
status: 'success',
|
||||
phase: 'progress',
|
||||
result: '첫 진행 상황',
|
||||
} as any);
|
||||
await controller.handleOutput({
|
||||
status: 'success',
|
||||
phase: 'progress',
|
||||
result: '둘째 진행 상황',
|
||||
} as any);
|
||||
await flushAsync();
|
||||
await controller.finish('success');
|
||||
|
||||
expect(channel.sendMessage).toHaveBeenCalledWith(
|
||||
'dc:test-room',
|
||||
expect.stringContaining('첫 진행 상황'),
|
||||
);
|
||||
expect(getAuditEntries()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
auditEvent: 'progress-fallback-send',
|
||||
chatJid: 'dc:test-room',
|
||||
runId: 'run-review-fallback',
|
||||
deliveryRole: 'reviewer',
|
||||
serviceId: 'codex-review',
|
||||
turnId: 'task-1:2026-04-10T14:22:00.000Z:reviewer-turn',
|
||||
channelName: 'discord-review',
|
||||
botUserId: 'bot-review',
|
||||
messageId: null,
|
||||
fallbackReason: 'tracked-send-error',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user