fix(telegram): split stop-created preview finalization path

Refactor lane preview finalization into explicit branches so stop-created
previews never duplicate sends when edit fails.

Add Telegram dispatch regressions for:
- stop-created preview edit failure (no duplicate send)
- existing preview edit failure (fallback send preserved)
- missing message id after stop-created flush (fallback send)

Thanks @obviyus for the original preview-prime direction in #27449.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
Peter Steinberger
2026-02-26 15:35:10 +00:00
parent 051fdcc428
commit f877e7e74c
2 changed files with 174 additions and 29 deletions

View File

@@ -416,6 +416,83 @@ describe("dispatchTelegramMessage draft streaming", () => {
expect(answerDraftStream.stop).toHaveBeenCalled();
});
it("does not duplicate final delivery when stop-created preview edit fails", async () => {
let messageId: number | undefined;
const draftStream = {
update: vi.fn(),
flush: vi.fn().mockResolvedValue(undefined),
messageId: vi.fn().mockImplementation(() => messageId),
clear: vi.fn().mockResolvedValue(undefined),
stop: vi.fn().mockImplementation(async () => {
messageId = 777;
}),
forceNewMessage: vi.fn(),
};
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver({ text: "Short final" }, { kind: "final" });
return { queuedFinal: true };
});
deliverReplies.mockResolvedValue({ delivered: true });
editMessageTelegram.mockRejectedValue(new Error("500: edit failed after stop flush"));
await dispatchWithContext({ context: createContext() });
expect(editMessageTelegram).toHaveBeenCalledWith(123, 777, "Short final", expect.any(Object));
expect(deliverReplies).not.toHaveBeenCalled();
expect(draftStream.stop).toHaveBeenCalled();
});
it("falls back to normal delivery when existing preview edit fails", async () => {
const draftStream = createDraftStream(999);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
async ({ dispatcherOptions, replyOptions }) => {
await replyOptions?.onPartialReply?.({ text: "Hel" });
await dispatcherOptions.deliver({ text: "Hello final" }, { kind: "final" });
return { queuedFinal: true };
},
);
deliverReplies.mockResolvedValue({ delivered: true });
editMessageTelegram.mockRejectedValue(new Error("500: preview edit failed"));
await dispatchWithContext({ context: createContext() });
expect(editMessageTelegram).toHaveBeenCalledWith(123, 999, "Hello final", expect.any(Object));
expect(deliverReplies).toHaveBeenCalledWith(
expect.objectContaining({
replies: [expect.objectContaining({ text: "Hello final" })],
}),
);
});
it("falls back to normal delivery when stop-created preview has no message id", async () => {
const draftStream = {
update: vi.fn(),
flush: vi.fn().mockResolvedValue(undefined),
messageId: vi.fn().mockReturnValue(undefined),
clear: vi.fn().mockResolvedValue(undefined),
stop: vi.fn().mockResolvedValue(undefined),
forceNewMessage: vi.fn(),
};
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver({ text: "Short final" }, { kind: "final" });
return { queuedFinal: true };
});
deliverReplies.mockResolvedValue({ delivered: true });
await dispatchWithContext({ context: createContext() });
expect(editMessageTelegram).not.toHaveBeenCalled();
expect(deliverReplies).toHaveBeenCalledWith(
expect.objectContaining({
replies: [expect.objectContaining({ text: "Short final" })],
}),
);
expect(draftStream.stop).toHaveBeenCalled();
});
it("does not overwrite finalized preview when additional final payloads are sent", async () => {
const draftStream = createDraftStream(999);
createTelegramDraftStream.mockReturnValue(draftStream);