mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 12:54:58 +00:00
fix: prevent Telegram preview stream cross-edit race (#23202)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 529abf209d
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { Bot } from "grammy";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTelegramDraftStream } from "./draft-stream.js";
|
||||
|
||||
@@ -18,8 +19,7 @@ function createThreadedDraftStream(
|
||||
thread: { id: number; scope: "forum" | "dm" },
|
||||
) {
|
||||
return createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
thread,
|
||||
});
|
||||
@@ -109,8 +109,7 @@ describe("createTelegramDraftStream", () => {
|
||||
deleteMessage: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
});
|
||||
|
||||
@@ -146,8 +145,7 @@ describe("createTelegramDraftStream", () => {
|
||||
deleteMessage: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
throttleMs: 1000,
|
||||
});
|
||||
@@ -167,11 +165,47 @@ describe("createTelegramDraftStream", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not rebind to an old message when forceNewMessage races an in-flight send", async () => {
|
||||
let resolveFirstSend: ((value: { message_id: number }) => void) | undefined;
|
||||
const firstSend = new Promise<{ message_id: number }>((resolve) => {
|
||||
resolveFirstSend = resolve;
|
||||
});
|
||||
const api = {
|
||||
sendMessage: vi.fn().mockReturnValueOnce(firstSend).mockResolvedValueOnce({ message_id: 42 }),
|
||||
editMessageText: vi.fn().mockResolvedValue(true),
|
||||
deleteMessage: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
const onSupersededPreview = vi.fn();
|
||||
const stream = createTelegramDraftStream({
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
onSupersededPreview,
|
||||
});
|
||||
|
||||
stream.update("Message A partial");
|
||||
await vi.waitFor(() => expect(api.sendMessage).toHaveBeenCalledTimes(1));
|
||||
|
||||
// Rotate to message B before message A send resolves.
|
||||
stream.forceNewMessage();
|
||||
stream.update("Message B partial");
|
||||
|
||||
resolveFirstSend?.({ message_id: 17 });
|
||||
await stream.flush();
|
||||
|
||||
expect(onSupersededPreview).toHaveBeenCalledWith({
|
||||
messageId: 17,
|
||||
textSnapshot: "Message A partial",
|
||||
parseMode: undefined,
|
||||
});
|
||||
expect(api.sendMessage).toHaveBeenCalledTimes(2);
|
||||
expect(api.sendMessage).toHaveBeenNthCalledWith(2, 123, "Message B partial", undefined);
|
||||
expect(api.editMessageText).not.toHaveBeenCalledWith(123, 17, "Message B partial");
|
||||
});
|
||||
|
||||
it("supports rendered previews with parse_mode", async () => {
|
||||
const api = createMockDraftApi();
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
renderText: (text) => ({ text: `<i>${text}</i>`, parseMode: "HTML" }),
|
||||
});
|
||||
@@ -191,8 +225,7 @@ describe("createTelegramDraftStream", () => {
|
||||
const api = createMockDraftApi();
|
||||
const warn = vi.fn();
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
maxChars: 100,
|
||||
renderText: () => ({ text: `<b>${"<".repeat(120)}</b>`, parseMode: "HTML" }),
|
||||
@@ -229,8 +262,7 @@ describe("draft stream initial message debounce", () => {
|
||||
it("sends immediately on stop() even with 1 character", async () => {
|
||||
const api = createMockApi();
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
minInitialChars: 30,
|
||||
});
|
||||
@@ -245,8 +277,7 @@ describe("draft stream initial message debounce", () => {
|
||||
it("sends immediately on stop() with short sentence", async () => {
|
||||
const api = createMockApi();
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
minInitialChars: 30,
|
||||
});
|
||||
@@ -263,8 +294,7 @@ describe("draft stream initial message debounce", () => {
|
||||
it("does not send first message below threshold", async () => {
|
||||
const api = createMockApi();
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
minInitialChars: 30,
|
||||
});
|
||||
@@ -278,8 +308,7 @@ describe("draft stream initial message debounce", () => {
|
||||
it("sends first message when reaching threshold", async () => {
|
||||
const api = createMockApi();
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
minInitialChars: 30,
|
||||
});
|
||||
@@ -294,8 +323,7 @@ describe("draft stream initial message debounce", () => {
|
||||
it("works with longer text above threshold", async () => {
|
||||
const api = createMockApi();
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
minInitialChars: 30,
|
||||
});
|
||||
@@ -311,8 +339,7 @@ describe("draft stream initial message debounce", () => {
|
||||
it("edits normally after first message is sent", async () => {
|
||||
const api = createMockApi();
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
minInitialChars: 30,
|
||||
});
|
||||
@@ -335,8 +362,7 @@ describe("draft stream initial message debounce", () => {
|
||||
it("sends immediately without minInitialChars set (backward compatible)", async () => {
|
||||
const api = createMockApi();
|
||||
const stream = createTelegramDraftStream({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
api: api as any,
|
||||
api: api as unknown as Bot["api"],
|
||||
chatId: 123,
|
||||
// no minInitialChars (backward-compatible behavior)
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user