diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index 3adc3198a4b..7efc056eba2 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -2780,6 +2780,7 @@ describe("dispatchTelegramMessage draft streaming", () => { Body: "abort", RawBody: "abort", CommandBody: "abort", + CommandAuthorized: true, } as never, }), }); @@ -2863,6 +2864,7 @@ describe("dispatchTelegramMessage draft streaming", () => { Body: "abort", RawBody: "abort", CommandBody: "abort", + CommandAuthorized: true, } as never, }), }); @@ -2933,6 +2935,7 @@ describe("dispatchTelegramMessage draft streaming", () => { Body: "abort", RawBody: "abort", CommandBody: "abort", + CommandAuthorized: true, } as never, }), }); @@ -3038,6 +3041,7 @@ describe("dispatchTelegramMessage draft streaming", () => { Body: "abort", RawBody: "abort", CommandBody: "abort", + CommandAuthorized: true, } as never, }), }); @@ -3130,6 +3134,7 @@ describe("dispatchTelegramMessage draft streaming", () => { Body: "abort", RawBody: "abort", CommandBody: "abort", + CommandAuthorized: true, } as never, }), }); @@ -3219,6 +3224,7 @@ describe("dispatchTelegramMessage draft streaming", () => { Body: "abort", RawBody: "abort", CommandBody: "abort", + CommandAuthorized: true, } as never, }), bot, @@ -3299,6 +3305,7 @@ describe("dispatchTelegramMessage draft streaming", () => { Body: "abort", RawBody: "abort", CommandBody: "abort", + CommandAuthorized: true, } as never, }), }); @@ -3439,6 +3446,89 @@ describe("dispatchTelegramMessage draft streaming", () => { ); }); + it("does not supersede the same session for unauthorized abort-looking commands", async () => { + let releaseFirstFinal!: () => void; + const firstFinalGate = new Promise((resolve) => { + releaseFirstFinal = resolve; + }); + let resolvePreviewVisible!: () => void; + const previewVisible = new Promise((resolve) => { + resolvePreviewVisible = resolve; + }); + + const firstAnswerDraft = createTestDraftStream({ + messageId: 1001, + onUpdate: (text) => { + if (text === "Old reply partial") { + resolvePreviewVisible(); + } + }, + }); + const firstReasoningDraft = createDraftStream(); + const unauthorizedAnswerDraft = createDraftStream(); + const unauthorizedReasoningDraft = createDraftStream(); + createTelegramDraftStream + .mockImplementationOnce(() => firstAnswerDraft) + .mockImplementationOnce(() => firstReasoningDraft) + .mockImplementationOnce(() => unauthorizedAnswerDraft) + .mockImplementationOnce(() => unauthorizedReasoningDraft); + dispatchReplyWithBufferedBlockDispatcher + .mockImplementationOnce(async ({ dispatcherOptions, replyOptions }) => { + await replyOptions?.onPartialReply?.({ text: "Old reply partial" }); + await firstFinalGate; + await dispatcherOptions.deliver({ text: "Old reply final" }, { kind: "final" }); + return { queuedFinal: true }; + }) + .mockImplementationOnce(async ({ dispatcherOptions }) => { + await dispatcherOptions.deliver({ text: "Unauthorized stop" }, { kind: "final" }); + return { queuedFinal: true }; + }); + deliverReplies.mockResolvedValue({ delivered: true }); + editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "1001" }); + + const firstPromise = dispatchWithContext({ + context: createContext({ + ctxPayload: { + SessionKey: "s1", + Body: "earlier request", + RawBody: "earlier request", + } as never, + }), + }); + + await previewVisible; + + const unauthorizedPromise = dispatchWithContext({ + context: createContext({ + ctxPayload: { + SessionKey: "s1", + Body: "/stop", + RawBody: "/stop", + CommandBody: "/stop", + CommandAuthorized: false, + } as never, + }), + }); + + await vi.waitFor(() => { + expect(deliverReplies).toHaveBeenCalledWith( + expect.objectContaining({ + replies: [{ text: "Unauthorized stop" }], + }), + ); + }); + + releaseFirstFinal(); + await Promise.all([firstPromise, unauthorizedPromise]); + + expect(editMessageTelegram).toHaveBeenCalledWith( + 123, + 1001, + "Old reply final", + expect.any(Object), + ); + }); + it("uses resolved DM config for auto-topic-label overrides", async () => { dispatchReplyWithBufferedBlockDispatcher.mockResolvedValue({ queuedFinal: true }); loadSessionStore.mockReturnValue({ s1: {} }); diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index 0abb9a6d03d..e2ba1b19ff5 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -475,12 +475,13 @@ export const dispatchTelegramMessage = async ({ : undefined; const chunkMode = resolveChunkMode(cfg, "telegram", route.accountId); + const shouldSupersedeAbortFence = + ctxPayload.CommandAuthorized && + isAbortRequestText(ctxPayload.CommandBody ?? ctxPayload.RawBody ?? ctxPayload.Body ?? ""); abortFenceGeneration = beginTelegramAbortFence({ key: dispatchFenceKey, - supersede: isAbortRequestText( - ctxPayload.CommandBody ?? ctxPayload.RawBody ?? ctxPayload.Body ?? "", - ), + supersede: shouldSupersedeAbortFence, }); const replyQuoteText =