feat(chat): add /stop cancellation command across gateway and telegram

This commit is contained in:
William Valentin
2026-02-19 09:52:57 -08:00
parent 027f7ad283
commit cd74b1e78a
9 changed files with 203 additions and 11 deletions
+42 -2
View File
@@ -81,14 +81,16 @@ describe('TelegramAdapter', () => {
// .use() for auth middleware
expect(mockUse).toHaveBeenCalledTimes(1);
expect(mockCatch).toHaveBeenCalledTimes(1);
// .command() for /start, /reset, /model, /local, /cloud, /transfer
expect(mockCommand).toHaveBeenCalledTimes(6);
// .command() for /start, /reset, /model, /local, /cloud, /transfer, /stop, /cancel
expect(mockCommand).toHaveBeenCalledTimes(8);
expect(mockCommand.mock.calls[0][0]).toBe('start');
expect(mockCommand.mock.calls[1][0]).toBe('reset');
expect(mockCommand.mock.calls[2][0]).toBe('model');
expect(mockCommand.mock.calls[3][0]).toBe('local');
expect(mockCommand.mock.calls[4][0]).toBe('cloud');
expect(mockCommand.mock.calls[5][0]).toBe('transfer');
expect(mockCommand.mock.calls[6][0]).toBe('stop');
expect(mockCommand.mock.calls[7][0]).toBe('cancel');
// .on('message:text', ...) for text handler
expect(mockOn).toHaveBeenCalledWith('message:text', expect.any(Function));
// .start() to begin long polling
@@ -301,6 +303,44 @@ describe('TelegramAdapter', () => {
});
});
it('/stop command forwards a stop command message', async () => {
const handler = vi.fn();
adapter.onMessage(handler);
await adapter.connect();
const stopHandler = getCommandHandler('stop');
const ctx = {
message: { message_id: 200 },
chat: { id: 100 },
from: { first_name: 'Will' },
};
await stopHandler(ctx);
expect(handler).toHaveBeenCalledTimes(1);
const msg: InboundMessage = handler.mock.calls[0][0];
expect(msg.text).toBe('/stop');
expect(msg.metadata).toEqual({ isCommand: true, command: 'stop' });
});
it('/cancel command forwards a cancel command message', async () => {
const handler = vi.fn();
adapter.onMessage(handler);
await adapter.connect();
const cancelHandler = getCommandHandler('cancel');
const ctx = {
message: { message_id: 201 },
chat: { id: 100 },
from: { first_name: 'Will' },
};
await cancelHandler(ctx);
expect(handler).toHaveBeenCalledTimes(1);
const msg: InboundMessage = handler.mock.calls[0][0];
expect(msg.text).toBe('/stop');
expect(msg.metadata).toEqual({ isCommand: true, command: 'cancel' });
});
// ── Auth middleware ───────────────────────────────────────────
it('auth middleware blocks unauthorized chat IDs', async () => {
+22 -4
View File
@@ -261,6 +261,22 @@ export class TelegramAdapter implements ChannelAdapter {
});
});
const handleStopCommand = (command: 'stop' | 'cancel') => async (ctx: { message?: { message_id?: number }; chat: { id: number }; from?: { first_name?: string } }) => {
if (!this.messageHandler) {return;}
this.messageHandler({
id: String(ctx.message?.message_id ?? Date.now()),
channel: 'telegram',
senderId: String(ctx.chat.id),
senderName: ctx.from?.first_name,
text: '/stop',
timestamp: Date.now(),
metadata: { isCommand: true, command },
});
};
bot.command('stop', handleStopCommand('stop'));
bot.command('cancel', handleStopCommand('cancel'));
// ── Text message handler ──
bot.on('message:text', async (ctx) => {
@@ -465,7 +481,7 @@ export class TelegramAdapter implements ChannelAdapter {
// bot.start() is a long-running method that resolves only when the bot stops.
// Do NOT await it — fire-and-forget so connect() resolves immediately.
bot.start({
const startResult = bot.start({
onStart: (botInfo) => {
console.log(`Telegram bot started: @${botInfo.username}`);
this.botInfo = { id: botInfo.id, username: botInfo.username };
@@ -474,7 +490,8 @@ export class TelegramAdapter implements ChannelAdapter {
this._lastError = undefined;
this._lastErrorAt = undefined;
},
}).catch((error) => {
});
Promise.resolve(startResult).catch((error) => {
const description = error instanceof Error ? error.message : String(error);
this.recordAdapterError(`Telegram connect failed: ${description}`);
this.scheduleReconnect();
@@ -633,7 +650,7 @@ export class TelegramAdapter implements ChannelAdapter {
this._status = 'connecting';
await bot.stop();
bot.start({
const startResult = bot.start({
onStart: (botInfo) => {
console.log(`Telegram bot reconnected: @${botInfo.username}`);
this.botInfo = { id: botInfo.id, username: botInfo.username };
@@ -642,7 +659,8 @@ export class TelegramAdapter implements ChannelAdapter {
this._lastError = undefined;
this._lastErrorAt = undefined;
},
}).catch((error) => {
});
Promise.resolve(startResult).catch((error) => {
const description = error instanceof Error ? error.message : String(error);
this.recordAdapterError(`Telegram reconnect polling error: ${description}`);
this.scheduleReconnect();