137 lines
3.7 KiB
TypeScript
137 lines
3.7 KiB
TypeScript
/**
|
|
* Mock Channel Adapter for E2E Testing
|
|
*
|
|
* Captures messages sent by the bot and allows simulating inbound messages.
|
|
*/
|
|
|
|
import type { ChannelAdapter } from '../channels/types.js';
|
|
import type { InboundMessage, OutboundMessage } from '../core/types.js';
|
|
import { parseCommand, HELP_TEXT } from '../core/commands.js';
|
|
|
|
export class MockChannelAdapter implements ChannelAdapter {
|
|
readonly id = 'mock' as const;
|
|
readonly name = 'Mock (Testing)';
|
|
|
|
private running = false;
|
|
private sentMessages: OutboundMessage[] = [];
|
|
private responseResolvers: Array<(msg: OutboundMessage) => void> = [];
|
|
|
|
onMessage?: (msg: InboundMessage) => Promise<void>;
|
|
onCommand?: (command: string, chatId?: string, args?: string) => Promise<string | null>;
|
|
|
|
async start(): Promise<void> {
|
|
this.running = true;
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
this.running = false;
|
|
}
|
|
|
|
isRunning(): boolean {
|
|
return this.running;
|
|
}
|
|
|
|
async sendMessage(msg: OutboundMessage): Promise<{ messageId: string }> {
|
|
const messageId = `mock-${Date.now()}`;
|
|
this.sentMessages.push(msg);
|
|
|
|
// Resolve any waiting promises
|
|
const resolver = this.responseResolvers.shift();
|
|
if (resolver) {
|
|
resolver(msg);
|
|
}
|
|
|
|
return { messageId };
|
|
}
|
|
|
|
async editMessage(_chatId: string, _messageId: string, _text: string): Promise<void> {
|
|
// No-op for mock
|
|
}
|
|
|
|
async sendTypingIndicator(_chatId: string): Promise<void> {
|
|
// No-op for mock
|
|
}
|
|
|
|
supportsEditing(): boolean {
|
|
return false; // Disable streaming edits for simpler testing
|
|
}
|
|
|
|
getFormatterHints() {
|
|
return { supportsReactions: false, supportsFiles: false };
|
|
}
|
|
|
|
/**
|
|
* Simulate an inbound message and wait for response
|
|
*/
|
|
async simulateMessage(
|
|
text: string,
|
|
options: {
|
|
userId?: string;
|
|
chatId?: string;
|
|
userName?: string;
|
|
timeoutMs?: number;
|
|
} = {}
|
|
): Promise<string> {
|
|
if (!this.onMessage) {
|
|
throw new Error('No message handler registered');
|
|
}
|
|
|
|
const chatId = options.chatId || 'test-chat-123';
|
|
|
|
// Handle slash commands locally (like real channels do)
|
|
const parsed = parseCommand(text);
|
|
if (parsed) {
|
|
if (parsed.command === 'help' || parsed.command === 'start') {
|
|
return HELP_TEXT;
|
|
} else if (this.onCommand) {
|
|
const result = await this.onCommand(parsed.command, chatId, parsed.args || undefined);
|
|
return result || '(No response)';
|
|
}
|
|
return '(Command not handled)';
|
|
}
|
|
|
|
// Create promise that resolves when bot sends response
|
|
const responsePromise = new Promise<OutboundMessage>((resolve) => {
|
|
this.responseResolvers.push(resolve);
|
|
});
|
|
|
|
// Send the inbound message
|
|
const inbound: InboundMessage = {
|
|
channel: 'mock',
|
|
chatId,
|
|
userId: options.userId || 'test-user-456',
|
|
userName: options.userName || 'Test User',
|
|
text,
|
|
timestamp: new Date(),
|
|
};
|
|
|
|
// Don't await - let it process async
|
|
this.onMessage(inbound).catch(err => {
|
|
console.error('[MockChannel] Error processing message:', err);
|
|
});
|
|
|
|
// Wait for response with timeout
|
|
const timeoutMs = options.timeoutMs ?? 60000;
|
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
setTimeout(() => reject(new Error(`Response timeout (${Math.round(timeoutMs / 1000)}s)`)), timeoutMs);
|
|
});
|
|
|
|
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
return response.text;
|
|
}
|
|
|
|
/**
|
|
* Get all sent messages (for assertions)
|
|
*/
|
|
getSentMessages(): OutboundMessage[] {
|
|
return [...this.sentMessages];
|
|
}
|
|
|
|
/**
|
|
* Clear sent messages
|
|
*/
|
|
clearMessages(): void {
|
|
this.sentMessages = [];
|
|
}
|
|
}
|