Files
Dirigent/test/discussion-hooks.test.ts

176 lines
5.8 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { registerBeforeMessageWriteHook } from '../plugin/hooks/before-message-write.ts';
import { registerMessageReceivedHook } from '../plugin/hooks/message-received.ts';
import { registerMessageSentHook } from '../plugin/hooks/message-sent.ts';
import { buildDiscussionOriginCallbackMessage } from '../plugin/core/discussion-messages.ts';
import { initTurnOrder, onNewMessage, getTurnDebugInfo, resetTurn } from '../plugin/turn-manager.ts';
type Handler = (event: Record<string, unknown>, ctx: Record<string, unknown>) => unknown;
function makeApi() {
const handlers = new Map<string, Handler>();
return {
handlers,
logger: {
info: (_msg: string) => {},
warn: (_msg: string) => {},
},
on(name: string, handler: Handler) {
handlers.set(name, handler);
},
};
}
test('before_message_write leaves ordinary channels dormant without sending a discussion idle reminder', async () => {
const channelId = 'normal-channel';
resetTurn(channelId);
initTurnOrder(channelId, ['agent-a', 'agent-b']);
onNewMessage(channelId, 'human-user', true);
const state = getTurnDebugInfo(channelId);
const [firstSpeaker, secondSpeaker] = state.turnOrder as string[];
assert.ok(firstSpeaker);
assert.ok(secondSpeaker);
const sessionChannelId = new Map<string, string>([
['sess-a', channelId],
['sess-b', channelId],
]);
const sessionAccountId = new Map<string, string>([
['sess-a', firstSpeaker],
['sess-b', secondSpeaker],
]);
const sessionAllowed = new Map<string, boolean>([
['sess-a', true],
['sess-b', true],
]);
const sessionTurnHandled = new Set<string>();
const idleReminderCalls: string[] = [];
const moderatorMessages: string[] = [];
const api = makeApi();
registerBeforeMessageWriteHook({
api: api as any,
baseConfig: { endSymbols: ['🔚'], moderatorBotToken: 'bot-token' } as any,
policyState: { channelPolicies: {} },
sessionAllowed,
sessionChannelId,
sessionAccountId,
sessionTurnHandled,
ensurePolicyStateLoaded: () => {},
shouldDebugLog: () => false,
ensureTurnOrder: () => {},
resolveDiscordUserId: () => undefined,
isMultiMessageMode: () => false,
sendModeratorMessage: async (_token, _channelId, content) => {
moderatorMessages.push(content);
return { ok: true };
},
discussionService: {
maybeSendIdleReminder: async (id) => {
idleReminderCalls.push(id);
},
getDiscussion: () => undefined,
},
});
const beforeMessageWrite = api.handlers.get('before_message_write');
assert.ok(beforeMessageWrite);
await beforeMessageWrite?.({ message: { role: 'assistant', content: 'NO_REPLY' } }, { sessionKey: 'sess-a' });
await beforeMessageWrite?.({ message: { role: 'assistant', content: 'NO_REPLY' } }, { sessionKey: 'sess-b' });
assert.deepEqual(idleReminderCalls, []);
assert.deepEqual(moderatorMessages, []);
assert.equal(getTurnDebugInfo(channelId).dormant, true);
});
test('message_received lets moderator discussion callback notifications wake the origin channel workflow', async () => {
const channelId = '1474327736242798612';
resetTurn(channelId);
initTurnOrder(channelId, ['agent-a', 'agent-b']);
assert.equal(getTurnDebugInfo(channelId).currentSpeaker, null);
const api = makeApi();
registerMessageReceivedHook({
api: api as any,
baseConfig: {
moderatorUserId: 'moderator-user',
humanList: ['human-user'],
} as any,
shouldDebugLog: () => false,
debugCtxSummary: () => ({}),
ensureTurnOrder: () => {},
getModeratorUserId: (cfg) => (cfg as any).moderatorUserId,
recordChannelAccount: () => false,
extractMentionedUserIds: () => [],
buildUserIdToAccountIdMap: () => new Map(),
enterMultiMessageMode: () => {},
exitMultiMessageMode: () => {},
discussionService: {
maybeReplyClosedChannel: async () => false,
},
});
const messageReceived = api.handlers.get('message_received');
assert.ok(messageReceived);
await messageReceived?.({
content: buildDiscussionOriginCallbackMessage('/workspace/plans/discussion-summary.md', 'discussion-42'),
from: 'moderator-user',
}, {
conversationId: channelId,
});
const state = getTurnDebugInfo(channelId);
assert.equal(state.currentSpeaker, state.turnOrder[0]);
assert.equal(state.dormant, false);
});
test('message_sent skips handoff after discuss-callback has closed the discussion channel', async () => {
const channelId = 'discussion-closed-channel';
resetTurn(channelId);
initTurnOrder(channelId, ['agent-a', 'agent-b']);
onNewMessage(channelId, 'human-user', true);
const state = getTurnDebugInfo(channelId);
const currentSpeaker = state.currentSpeaker as string;
assert.ok(currentSpeaker);
const moderatorMessages: string[] = [];
const api = makeApi();
registerMessageSentHook({
api: api as any,
baseConfig: {
endSymbols: ['🔚'],
moderatorBotToken: 'bot-token',
schedulingIdentifier: '➡️',
} as any,
policyState: { channelPolicies: {} },
sessionChannelId: new Map([['sess-closed', channelId]]),
sessionAccountId: new Map([['sess-closed', currentSpeaker]]),
sessionTurnHandled: new Set(),
ensurePolicyStateLoaded: () => {},
resolveDiscordUserId: () => 'discord-user-next',
sendModeratorMessage: async (_token, _channelId, content) => {
moderatorMessages.push(content);
return { ok: true };
},
discussionService: {
isClosedDiscussion: (id) => id === channelId,
},
});
const messageSent = api.handlers.get('message_sent');
assert.ok(messageSent);
await messageSent?.({ content: 'NO_REPLY' }, { sessionKey: 'sess-closed', accountId: currentSpeaker, channelId });
assert.deepEqual(moderatorMessages, []);
assert.equal(getTurnDebugInfo(channelId).currentSpeaker, currentSpeaker);
});