feat(telegram): use sendMessageDraft for private chat streaming (#31824)
* feat(telegram): use sendMessageDraft for private stream previews * test(telegram): cover DM draft id rotation race * fix(telegram): keep DM reasoning updates in draft preview * fix(telegram): split DM reasoning preview transport * fix(telegram): harden DM draft preview fallback paths * style(telegram): normalize draft preview formatting
This commit is contained in:
@@ -53,10 +53,15 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
});
|
||||
|
||||
function createDraftStream(messageId?: number) {
|
||||
let previewRevision = 0;
|
||||
return {
|
||||
update: vi.fn(),
|
||||
flush: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockImplementation(() => {
|
||||
previewRevision += 1;
|
||||
}),
|
||||
flush: vi.fn().mockResolvedValue(true),
|
||||
messageId: vi.fn().mockReturnValue(messageId),
|
||||
previewMode: vi.fn().mockReturnValue("message"),
|
||||
previewRevision: vi.fn().mockImplementation(() => previewRevision),
|
||||
clear: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
forceNewMessage: vi.fn(),
|
||||
@@ -66,14 +71,18 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
function createSequencedDraftStream(startMessageId = 1001) {
|
||||
let activeMessageId: number | undefined;
|
||||
let nextMessageId = startMessageId;
|
||||
let previewRevision = 0;
|
||||
return {
|
||||
update: vi.fn().mockImplementation(() => {
|
||||
if (activeMessageId == null) {
|
||||
activeMessageId = nextMessageId++;
|
||||
}
|
||||
previewRevision += 1;
|
||||
}),
|
||||
flush: vi.fn().mockResolvedValue(undefined),
|
||||
flush: vi.fn().mockResolvedValue(true),
|
||||
messageId: vi.fn().mockImplementation(() => activeMessageId),
|
||||
previewMode: vi.fn().mockReturnValue("message"),
|
||||
previewRevision: vi.fn().mockImplementation(() => previewRevision),
|
||||
clear: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
forceNewMessage: vi.fn().mockImplementation(() => {
|
||||
@@ -1084,6 +1093,36 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("uses message preview transport for DM reasoning lane when answer preview lane is active", async () => {
|
||||
setupDraftStreams({ answerMessageId: 999, reasoningMessageId: 111 });
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||
async ({ dispatcherOptions, replyOptions }) => {
|
||||
await replyOptions?.onReasoningStream?.({ text: "Reasoning:\n_Working on it..._" });
|
||||
await replyOptions?.onPartialReply?.({ text: "Checking the directory..." });
|
||||
await dispatcherOptions.deliver({ text: "Checking the directory..." }, { kind: "final" });
|
||||
return { queuedFinal: true };
|
||||
},
|
||||
);
|
||||
deliverReplies.mockResolvedValue({ delivered: true });
|
||||
editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "999" });
|
||||
|
||||
await dispatchWithContext({ context: createReasoningStreamContext(), streamMode: "partial" });
|
||||
|
||||
expect(createTelegramDraftStream).toHaveBeenCalledTimes(2);
|
||||
expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
thread: { id: 777, scope: "dm" },
|
||||
previewTransport: "auto",
|
||||
}),
|
||||
);
|
||||
expect(createTelegramDraftStream.mock.calls[1]?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
thread: { id: 777, scope: "dm" },
|
||||
previewTransport: "message",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps reasoning and answer streaming in separate preview lanes", async () => {
|
||||
const { answerDraftStream, reasoningDraftStream } = setupDraftStreams({
|
||||
answerMessageId: 999,
|
||||
@@ -1218,6 +1257,98 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps DM draft reasoning block updates in preview flow without sending duplicates", async () => {
|
||||
const answerDraftStream = createDraftStream(999);
|
||||
let previewRevision = 0;
|
||||
const reasoningDraftStream = {
|
||||
update: vi.fn(),
|
||||
flush: vi.fn().mockResolvedValue(true),
|
||||
messageId: vi.fn().mockReturnValue(undefined),
|
||||
previewMode: vi.fn().mockReturnValue("draft"),
|
||||
previewRevision: vi.fn().mockImplementation(() => previewRevision),
|
||||
clear: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
forceNewMessage: vi.fn(),
|
||||
};
|
||||
reasoningDraftStream.update.mockImplementation(() => {
|
||||
previewRevision += 1;
|
||||
});
|
||||
createTelegramDraftStream
|
||||
.mockImplementationOnce(() => answerDraftStream)
|
||||
.mockImplementationOnce(() => reasoningDraftStream);
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||
async ({ dispatcherOptions, replyOptions }) => {
|
||||
await replyOptions?.onReasoningStream?.({
|
||||
text: "Reasoning:\nI am counting letters...",
|
||||
});
|
||||
await replyOptions?.onReasoningEnd?.();
|
||||
await replyOptions?.onPartialReply?.({ text: "3" });
|
||||
await dispatcherOptions.deliver({ text: "3" }, { kind: "final" });
|
||||
await dispatcherOptions.deliver(
|
||||
{
|
||||
text: "Reasoning:\nI am counting letters. The total is 3.",
|
||||
},
|
||||
{ kind: "block" },
|
||||
);
|
||||
return { queuedFinal: true };
|
||||
},
|
||||
);
|
||||
deliverReplies.mockResolvedValue({ delivered: true });
|
||||
editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "999" });
|
||||
|
||||
await dispatchWithContext({ context: createReasoningStreamContext(), streamMode: "partial" });
|
||||
|
||||
expect(editMessageTelegram).toHaveBeenCalledWith(123, 999, "3", expect.any(Object));
|
||||
expect(reasoningDraftStream.update).toHaveBeenCalledWith(
|
||||
"Reasoning:\nI am counting letters. The total is 3.",
|
||||
);
|
||||
expect(reasoningDraftStream.flush).toHaveBeenCalled();
|
||||
expect(deliverReplies).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
replies: [expect.objectContaining({ text: expect.stringContaining("Reasoning:\nI am") })],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to normal send when DM draft reasoning flush emits no preview update", async () => {
|
||||
const answerDraftStream = createDraftStream(999);
|
||||
const previewRevision = 0;
|
||||
const reasoningDraftStream = {
|
||||
update: vi.fn(),
|
||||
flush: vi.fn().mockResolvedValue(false),
|
||||
messageId: vi.fn().mockReturnValue(undefined),
|
||||
previewMode: vi.fn().mockReturnValue("draft"),
|
||||
previewRevision: vi.fn().mockReturnValue(previewRevision),
|
||||
clear: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
forceNewMessage: vi.fn(),
|
||||
};
|
||||
createTelegramDraftStream
|
||||
.mockImplementationOnce(() => answerDraftStream)
|
||||
.mockImplementationOnce(() => reasoningDraftStream);
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||
async ({ dispatcherOptions, replyOptions }) => {
|
||||
await replyOptions?.onReasoningStream?.({ text: "Reasoning:\n_step one_" });
|
||||
await replyOptions?.onReasoningEnd?.();
|
||||
await dispatcherOptions.deliver(
|
||||
{ text: "Reasoning:\n_step one expanded_" },
|
||||
{ kind: "block" },
|
||||
);
|
||||
return { queuedFinal: true };
|
||||
},
|
||||
);
|
||||
deliverReplies.mockResolvedValue({ delivered: true });
|
||||
|
||||
await dispatchWithContext({ context: createReasoningStreamContext(), streamMode: "partial" });
|
||||
|
||||
expect(reasoningDraftStream.flush).toHaveBeenCalled();
|
||||
expect(deliverReplies).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
replies: [expect.objectContaining({ text: "Reasoning:\n_step one expanded_" })],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("routes think-tag partials to reasoning lane and keeps answer lane clean", async () => {
|
||||
const { answerDraftStream, reasoningDraftStream } = setupDraftStreams({
|
||||
answerMessageId: 999,
|
||||
|
||||
Reference in New Issue
Block a user