feat(chat): add /stop cancellation command across gateway and telegram
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user