feat: add codex review failover and suppress output hardening
This commit is contained in:
443
src/duplicate-suppression.test.ts
Normal file
443
src/duplicate-suppression.test.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
_initTestDatabase,
|
||||
storeMessage,
|
||||
storeChatMetadata,
|
||||
createProducedWorkItem,
|
||||
getOpenWorkItem,
|
||||
setRegisteredGroup,
|
||||
isPairedRoomJid,
|
||||
getLastBotFinalMessage,
|
||||
markWorkItemDelivered,
|
||||
} from './db.js';
|
||||
import { normalizeMessageForDedupe } from './router.js';
|
||||
import { isDuplicateOfLastBotFinal } from './message-runtime.js';
|
||||
|
||||
beforeEach(() => {
|
||||
_initTestDatabase();
|
||||
});
|
||||
|
||||
// Helper to create a message
|
||||
const createMessage = (overrides: Partial<{
|
||||
id: string;
|
||||
chat_jid: string;
|
||||
sender: string;
|
||||
sender_name: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
is_from_me: boolean;
|
||||
is_bot_message: boolean;
|
||||
}> = {}) => ({
|
||||
id: overrides.id ?? 'msg-1',
|
||||
chat_jid: overrides.chat_jid ?? 'dc:test-room',
|
||||
sender: overrides.sender ?? 'user1',
|
||||
sender_name: overrides.sender_name ?? 'User',
|
||||
content: overrides.content ?? 'Hello',
|
||||
timestamp: overrides.timestamp ?? new Date().toISOString(),
|
||||
is_from_me: overrides.is_from_me ?? false,
|
||||
is_bot_message: overrides.is_bot_message ?? false,
|
||||
});
|
||||
|
||||
// Helper to setup chat metadata
|
||||
const setupChat = (jid: string) => {
|
||||
storeChatMetadata(jid, new Date().toISOString(), 'Test Chat', 'discord', true);
|
||||
};
|
||||
|
||||
describe('isPairedRoomJid', () => {
|
||||
it('returns true when both claude-code and codex are registered', () => {
|
||||
const jid = 'dc:paired-room';
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@claude',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@codex',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'codex',
|
||||
});
|
||||
|
||||
expect(isPairedRoomJid(jid)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when only claude-code is registered', () => {
|
||||
const jid = 'dc:single-claude';
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Single Claude',
|
||||
folder: 'single-claude',
|
||||
trigger: '@claude',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
|
||||
expect(isPairedRoomJid(jid)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when only codex is registered', () => {
|
||||
const jid = 'dc:single-codex';
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Single Codex',
|
||||
folder: 'single-codex',
|
||||
trigger: '@codex',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'codex',
|
||||
});
|
||||
|
||||
expect(isPairedRoomJid(jid)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLastBotFinalMessage', () => {
|
||||
it('returns the most recent bot message from any service (is_bot_message=1)', () => {
|
||||
const jid = 'dc:test-room';
|
||||
const now = new Date();
|
||||
setupChat(jid);
|
||||
|
||||
// Store older bot message (from any bot)
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-1',
|
||||
chat_jid: jid,
|
||||
content: 'First bot message',
|
||||
timestamp: new Date(now.getTime() - 1000).toISOString(),
|
||||
is_from_me: true,
|
||||
is_bot_message: true,
|
||||
}));
|
||||
|
||||
// Store newer bot message (from different bot - is_from_me=0 but is_bot_message=1)
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-2',
|
||||
chat_jid: jid,
|
||||
content: 'Second bot message',
|
||||
timestamp: now.toISOString(),
|
||||
is_from_me: false, // Different bot
|
||||
is_bot_message: true,
|
||||
}));
|
||||
|
||||
// Store human message (should not be returned)
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-3',
|
||||
chat_jid: jid,
|
||||
content: 'Human message',
|
||||
timestamp: new Date(now.getTime() + 1000).toISOString(),
|
||||
is_from_me: false,
|
||||
is_bot_message: false,
|
||||
}));
|
||||
|
||||
const lastMessages = getLastBotFinalMessage(jid, 'claude-code', 1);
|
||||
expect(lastMessages).toHaveLength(1);
|
||||
expect(lastMessages[0].content).toBe('Second bot message');
|
||||
});
|
||||
|
||||
it('returns empty array when no bot messages exist', () => {
|
||||
const jid = 'dc:empty-room';
|
||||
setupChat(jid);
|
||||
|
||||
const lastMessages = getLastBotFinalMessage(jid, 'claude-code', 1);
|
||||
expect(lastMessages).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeMessageForDedupe', () => {
|
||||
it('normalizes messages for comparison', () => {
|
||||
expect(normalizeMessageForDedupe(' Hello World ')).toBe('hello world');
|
||||
expect(normalizeMessageForDedupe('Hello\n\nWorld')).toBe('hello world');
|
||||
expect(normalizeMessageForDedupe('Hello World')).toBe('hello world');
|
||||
expect(normalizeMessageForDedupe('HELLO WORLD')).toBe('hello world');
|
||||
});
|
||||
|
||||
it('handles empty strings', () => {
|
||||
expect(normalizeMessageForDedupe('')).toBe('');
|
||||
expect(normalizeMessageForDedupe(' ')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDuplicateOfLastBotFinal (runtime function)', () => {
|
||||
it('paired room: detects duplicate final message', () => {
|
||||
const jid = 'dc:paired-room';
|
||||
setupChat(jid);
|
||||
|
||||
// Register as paired room
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@claude',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@codex',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'codex',
|
||||
});
|
||||
|
||||
// Store a bot message (from any bot)
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-1',
|
||||
chat_jid: jid,
|
||||
content: 'DONE — Task completed successfully',
|
||||
timestamp: new Date().toISOString(),
|
||||
is_from_me: false, // Different bot
|
||||
is_bot_message: true,
|
||||
}));
|
||||
|
||||
// Verify it's a paired room
|
||||
expect(isPairedRoomJid(jid)).toBe(true);
|
||||
|
||||
// Verify duplicate detection works via the actual runtime function
|
||||
expect(isDuplicateOfLastBotFinal(jid, 'DONE — Task completed successfully')).toBe(true);
|
||||
expect(isDuplicateOfLastBotFinal(jid, 'done — task completed successfully')).toBe(true); // Normalized match
|
||||
});
|
||||
|
||||
it('non-paired room: duplicate check is bypassed', () => {
|
||||
const jid = 'dc:single-bot';
|
||||
setupChat(jid);
|
||||
|
||||
// Register only one bot
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Single Bot',
|
||||
folder: 'single-bot',
|
||||
trigger: '@claude',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
|
||||
// Store a bot message
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-1',
|
||||
chat_jid: jid,
|
||||
content: 'DONE — Task completed',
|
||||
timestamp: new Date().toISOString(),
|
||||
is_from_me: true,
|
||||
is_bot_message: true,
|
||||
}));
|
||||
|
||||
// Verify it's NOT a paired room
|
||||
expect(isPairedRoomJid(jid)).toBe(false);
|
||||
|
||||
// Duplicate check should be bypassed (return false)
|
||||
expect(isDuplicateOfLastBotFinal(jid, 'DONE — Task completed')).toBe(false);
|
||||
});
|
||||
|
||||
it('non-duplicate message in paired room is not suppressed', () => {
|
||||
const jid = 'dc:paired-room';
|
||||
setupChat(jid);
|
||||
|
||||
// Register as paired room
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@claude',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@codex',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'codex',
|
||||
});
|
||||
|
||||
// Store a bot message
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-1',
|
||||
chat_jid: jid,
|
||||
content: 'First message content',
|
||||
timestamp: new Date().toISOString(),
|
||||
is_from_me: true,
|
||||
is_bot_message: true,
|
||||
}));
|
||||
|
||||
// Verify different content is not a duplicate
|
||||
expect(isDuplicateOfLastBotFinal(jid, 'Different message content')).toBe(false);
|
||||
expect(isDuplicateOfLastBotFinal(jid, 'First message content')).toBe(true);
|
||||
});
|
||||
|
||||
it('cross-bot duplicate detection: claude detects codex message as duplicate', () => {
|
||||
const jid = 'dc:paired-room';
|
||||
setupChat(jid);
|
||||
|
||||
// Register as paired room
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@claude',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@codex',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'codex',
|
||||
});
|
||||
|
||||
// Store a bot message from "codex" (is_from_me=0)
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-1',
|
||||
chat_jid: jid,
|
||||
content: 'DONE — Analysis complete',
|
||||
timestamp: new Date().toISOString(),
|
||||
is_from_me: false, // Other bot
|
||||
is_bot_message: true,
|
||||
}));
|
||||
|
||||
// Verify claude service detects this as duplicate (cross-bot detection)
|
||||
expect(isDuplicateOfLastBotFinal(jid, 'DONE — Analysis complete')).toBe(true);
|
||||
});
|
||||
|
||||
it('normalization handles whitespace and case differences', () => {
|
||||
const jid = 'dc:paired-room';
|
||||
setupChat(jid);
|
||||
|
||||
// Register as paired room
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@claude',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@codex',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'codex',
|
||||
});
|
||||
|
||||
// Store a bot message with specific formatting
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-1',
|
||||
chat_jid: jid,
|
||||
content: 'DONE — Task completed\n\nSuccessfully',
|
||||
timestamp: new Date().toISOString(),
|
||||
is_from_me: true,
|
||||
is_bot_message: true,
|
||||
}));
|
||||
|
||||
// Same content with different whitespace should be detected as duplicate
|
||||
expect(isDuplicateOfLastBotFinal(jid, 'done — task completed successfully')).toBe(true);
|
||||
expect(isDuplicateOfLastBotFinal(jid, ' DONE — Task completed Successfully ')).toBe(true);
|
||||
|
||||
// Different content should not be duplicate
|
||||
expect(isDuplicateOfLastBotFinal(jid, 'FAILED — Task failed')).toBe(false);
|
||||
});
|
||||
|
||||
it('work item lifecycle: produced -> delivered (duplicate)', () => {
|
||||
const jid = 'dc:paired-room';
|
||||
setupChat(jid);
|
||||
|
||||
// Register as paired room
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@claude',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@codex',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'codex',
|
||||
});
|
||||
|
||||
// Store a bot message (simulating previous delivery)
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-1',
|
||||
chat_jid: jid,
|
||||
content: 'DONE — Task completed successfully',
|
||||
timestamp: new Date().toISOString(),
|
||||
is_from_me: true,
|
||||
is_bot_message: true,
|
||||
}));
|
||||
|
||||
// Create a work item with the same content (duplicate)
|
||||
const workItem = createProducedWorkItem({
|
||||
group_folder: 'paired-room',
|
||||
chat_jid: jid,
|
||||
agent_type: 'claude-code',
|
||||
start_seq: 1,
|
||||
end_seq: 2,
|
||||
result_payload: 'DONE — Task completed successfully',
|
||||
});
|
||||
|
||||
expect(workItem.status).toBe('produced');
|
||||
|
||||
// Verify duplicate detection via actual runtime function
|
||||
expect(isDuplicateOfLastBotFinal(jid, workItem.result_payload)).toBe(true);
|
||||
|
||||
// Simulate suppression: mark as delivered without sending
|
||||
markWorkItemDelivered(workItem.id, null);
|
||||
|
||||
// Verify work item is marked delivered
|
||||
const openItem = getOpenWorkItem(jid, 'claude-code');
|
||||
expect(openItem).toBeUndefined(); // No open items because it was marked delivered
|
||||
});
|
||||
|
||||
it('work item lifecycle: produced -> pending -> delivered (non-duplicate)', () => {
|
||||
const jid = 'dc:paired-room';
|
||||
setupChat(jid);
|
||||
|
||||
// Register as paired room
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@claude',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
|
||||
setRegisteredGroup(jid, {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@codex',
|
||||
added_at: new Date().toISOString(),
|
||||
agentType: 'codex',
|
||||
});
|
||||
|
||||
// Store a bot message
|
||||
storeMessage(createMessage({
|
||||
id: 'msg-1',
|
||||
chat_jid: jid,
|
||||
content: 'First message',
|
||||
timestamp: new Date().toISOString(),
|
||||
is_from_me: true,
|
||||
is_bot_message: true,
|
||||
}));
|
||||
|
||||
// Create a work item with DIFFERENT content (non-duplicate)
|
||||
const workItem = createProducedWorkItem({
|
||||
group_folder: 'paired-room',
|
||||
chat_jid: jid,
|
||||
agent_type: 'claude-code',
|
||||
start_seq: 1,
|
||||
end_seq: 2,
|
||||
result_payload: 'Second message (different)',
|
||||
});
|
||||
|
||||
expect(workItem.status).toBe('produced');
|
||||
|
||||
// Verify NOT a duplicate
|
||||
expect(isDuplicateOfLastBotFinal(jid, workItem.result_payload)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user