import { setReplyPayloadMetadata } from "openclaw/plugin-sdk/reply-payload-testing";
import { expect, it, vi } from "vitest";
import {
describeTelegramDispatch,
appendAssistantMirrorMessageByIdentity,
createContext,
createDraftStream,
createReasoningDefaultContext,
createReasoningForumTopicContext,
createReasoningStreamContext,
createSequencedDraftStream,
createStatusReactionController,
createTelegramDraftStream,
deliverReplies,
dispatchReplyWithBufferedBlockDispatcher,
dispatchWithContext,
editMessageTelegram,
emitInternalMessageSentHook,
expectDeliveredReply,
expectDraftStreamParams,
expectRecordFields,
loadSessionStore,
mockCallArg,
readLatestAssistantTextByIdentity,
recordOutboundMessageForPromptContext,
requireInvocationOrder,
setupDraftStreams,
telegramProgressPreview,
} from "./bot-message-dispatch.test-harness.js";
import type { TelegramMessageContext } from "./bot-message-dispatch.test-harness.js";
describeTelegramDispatch("dispatchTelegramMessage progress-rendering", () => {
it("renders typed plan updates as a live checklist", async () => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onPlanUpdate?.({
phase: "update",
explanation: "Implementing the change.",
steps: [
{ step: "Inspect", status: "completed" },
{ step: "Patch", status: "in_progress" },
{ step: "Test", status: "pending" },
],
});
return { queuedFinal: false };
});
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress", progress: { label: false } } },
});
expect(draftStream.updatePreview).toHaveBeenLastCalledWith(
telegramProgressPreview(
"Implementing the change.\n\n✅ Inspect\n▸ Patch\n▢ Test",
"Implementing the change.
✅ Inspect
▸ Patch
▢ Test",
),
);
});
it("renders plan checklists when the explanation is omitted", async () => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onPlanUpdate?.({
phase: "update",
steps: [
{ step: "Patch", status: "in_progress" },
{ step: "Test", status: "pending" },
],
});
return { queuedFinal: false };
});
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress", progress: { label: false } } },
});
expect(draftStream.updatePreview).toHaveBeenLastCalledWith(
telegramProgressPreview("▸ Patch\n▢ Test", "▸ Patch
▢ Test"),
);
});
it("renders the headline immediately when the preamble arrives after tool progress", async () => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onReplyStart?.();
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
// The first valid preamble after the draft opened must render as the
// status headline in the same push, not wait for another progress event.
await replyOptions?.onItemEvent?.({
kind: "preamble",
itemId: "preamble-1",
progressText: "Checking recent context",
});
return { queuedFinal: false };
});
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: {
streaming: { mode: "progress", progress: { label: "Shelling" } },
},
});
expect(draftStream.updatePreview).toHaveBeenLastCalledWith(
telegramProgressPreview(
"Shelling\n\nChecking recent context\n🛠️ Exec",
"Shelling\nChecking recent context\n🛠️ Exec",
),
);
});
it("keeps memory-search tool rows beneath a preamble", async () => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onReplyStart?.();
await replyOptions?.onItemEvent?.({
kind: "preamble",
itemId: "preamble-1",
progressText: "Checking memory",
});
await replyOptions?.onToolStart?.({
name: "memory_search",
phase: "start",
toolCallId: "memory-search-1",
args: { query: "release status" },
});
return { queuedFinal: false };
});
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress" } },
});
const preview = draftStream.updatePreview.mock.calls.at(-1)?.[0];
expect(preview?.text).toContain("Checking memory");
expect(preview?.text).toContain("Memory Search");
});
it("keeps the progress draft label when tool progress lines are hidden", async () => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onReplyStart?.();
await replyOptions?.onAssistantMessageStart?.();
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
return { queuedFinal: false };
});
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: {
streaming: {
mode: "progress",
progress: { label: "Shelling", toolProgress: false },
},
},
});
expect(draftStream.updatePreview).toHaveBeenCalledWith(
telegramProgressPreview("Shelling", "Shelling"),
);
expect(draftStream.flush).toHaveBeenCalled();
});
it("keeps streamed reasoning visible when tool progress lines are hidden", async () => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onReplyStart?.();
await replyOptions?.onAssistantMessageStart?.();
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
await replyOptions?.onReasoningStream?.({ text: "Checking files" });
return { queuedFinal: false };
});
await dispatchWithContext({
context: createReasoningStreamContext(),
streamMode: "progress",
telegramCfg: {
streaming: {
mode: "progress",
progress: { label: "Shelling", toolProgress: false },
},
},
});
expect(draftStream.updatePreview).toHaveBeenCalledWith(
telegramProgressPreview(
"Shelling\n\n🧠 Checking files",
"Shelling\n🧠 Checking files",
),
);
});
it.each([{ label: false }, { label: "Shelling", maxLines: 1 }] as const)(
"does not duplicate Telegram progress HTML rows without a visible label",
async (progress) => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onReplyStart?.();
await replyOptions?.onAssistantMessageStart?.();
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
return { queuedFinal: false };
});
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: {
streaming: {
mode: "progress",
progress,
},
},
});
expect(draftStream.updatePreview).toHaveBeenCalledWith(
telegramProgressPreview("🛠️ Exec", "🛠️ Exec"),
);
},
);
it("keeps progress draft labels static while the draft is active", async () => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
let finishRun: (() => void) | undefined;
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onReplyStart?.();
await replyOptions?.onAssistantMessageStart?.();
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
await new Promise((resolve) => {
finishRun = resolve;
});
return { queuedFinal: false };
});
const run = dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: {
streaming: {
mode: "progress",
progress: { label: "Working", toolProgress: false },
},
},
});
await vi.waitFor(() =>
expect(draftStream.updatePreview).toHaveBeenCalledWith(
telegramProgressPreview("Working", "Working"),
),
);
expect(draftStream.updatePreview).not.toHaveBeenCalledWith({ text: "Working." });
expect(draftStream.updatePreview).not.toHaveBeenCalledWith({ text: "Working.." });
expect(draftStream.updatePreview).not.toHaveBeenCalledWith({ text: "Working..." });
finishRun?.();
await run;
});
it("renders Telegram progress drafts before slow status reactions resolve", async () => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
let releaseSetTool: (() => void) | undefined;
const statusReactionController = createStatusReactionController();
statusReactionController.setTool.mockImplementation(
() =>
new Promise((resolve) => {
releaseSetTool = resolve;
}),
);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
const pendingToolStart = replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
await Promise.resolve();
await Promise.resolve();
const updateBeforeStatusReaction = draftStream.updatePreview.mock.calls.at(-1)?.[0]?.text;
releaseSetTool?.();
await pendingToolStart;
expect(updateBeforeStatusReaction).toBe("Shelling
🛠️ Exec");
return { queuedFinal: false };
});
await dispatchWithContext({
context: createContext({
statusReactionController: statusReactionController as never,
}),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress", progress: { label: "Shelling" } } },
});
expect(statusReactionController.setTool).toHaveBeenCalledWith("exec");
});
it("keeps non-command Telegram progress draft lines across post-tool assistant boundaries", async () => {
vi.useFakeTimers();
try {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
async ({ dispatcherOptions, replyOptions }) => {
await replyOptions?.onReplyStart?.();
await replyOptions?.onAssistantMessageStart?.();
await replyOptions?.onItemEvent?.({ kind: "search", progressText: "docs lookup" });
await vi.advanceTimersByTimeAsync(5_000);
await replyOptions?.onItemEvent?.({ progressText: "tests passed" });
await replyOptions?.onAssistantMessageStart?.();
await dispatcherOptions.deliver({ text: "Final after tool" }, { kind: "final" });
return { queuedFinal: true };
},
);
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress", progress: { label: "Shelling" } } },
});
expect(draftStream.updatePreview).toHaveBeenCalledWith(
telegramProgressPreview(
"Shelling\n\n🔎 Web Search: docs lookup\n• tests passed",
"Shelling\n🔎 Web Search docs lookup\nUpdate tests passed",
),
);
// A tool-progress-only window with nothing to summarize is torn down via the
// deferred-delete reposition (new content first, delete later), not a bare
// immediate clear/delete or forceNewMessage.
expect(draftStream.rotateToNewMessageDeferringDelete).toHaveBeenCalledTimes(1);
expect(draftStream.forceNewMessage).not.toHaveBeenCalled();
expect(draftStream.clear).not.toHaveBeenCalled();
expectDeliveredReply(0, { text: "Final after tool" });
expect(editMessageTelegram).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
}
});
it("falls back to normal send for error payloads and clears the pending stream", async () => {
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver({ text: "Boom", isError: true }, { kind: "final" });
return { queuedFinal: true };
});
await dispatchWithContext({ context: createContext() });
expect(answerDraftStream.clear).toHaveBeenCalled();
expectDeliveredReply(0, { text: "Boom" });
});
it("suppresses failed tool payloads after the final reply", async () => {
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver({ text: "Final answer" }, { kind: "final" });
await dispatcherOptions.deliver(
{ text: "Tool failed after final", isError: true },
{ kind: "tool" },
);
return { queuedFinal: true };
});
await dispatchWithContext({ context: createContext(), streamMode: "off" });
expect(deliverReplies).toHaveBeenCalledTimes(1);
expectDeliveredReply(0, { text: "Final answer" });
});
it("preserves final error warnings after the final reply", async () => {
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver({ text: "Final answer" }, { kind: "final" });
await dispatcherOptions.deliver({ text: "Write failed", isError: true }, { kind: "final" });
return { queuedFinal: true };
});
await dispatchWithContext({ context: createContext(), streamMode: "off" });
expect(deliverReplies).toHaveBeenCalledTimes(2);
expectDeliveredReply(0, { text: "Final answer" });
expectDeliveredReply(0, { text: "Write failed", isError: true }, 1);
});
it("suppresses non-terminal final error warnings after the final reply", async () => {
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver({ text: "Final answer" }, { kind: "final" });
await dispatcherOptions.deliver(
setReplyPayloadMetadata(
{ text: "Post-processing failed", isError: true },
{ nonTerminalToolErrorWarning: true },
),
{ kind: "final" },
);
return { queuedFinal: true };
});
await dispatchWithContext({ context: createContext(), streamMode: "off" });
expect(deliverReplies).toHaveBeenCalledTimes(1);
expectDeliveredReply(0, { text: "Final answer" });
});
it("preserves non-terminal final error warnings before any final reply is delivered", async () => {
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver(
setReplyPayloadMetadata(
{ text: "Post-processing failed", isError: true },
{ nonTerminalToolErrorWarning: true },
),
{ kind: "final" },
);
return { queuedFinal: true };
});
await dispatchWithContext({ context: createContext(), streamMode: "off" });
expect(deliverReplies).toHaveBeenCalledTimes(1);
expectDeliveredReply(0, { text: "Post-processing failed", isError: true });
});
it("streams button-bearing text into the same message", async () => {
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
const buttons = [[{ text: "OK", callback_data: "ok" }]];
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver(
{ text: "Choose", channelData: { telegram: { buttons } } },
{ kind: "final" },
);
return { queuedFinal: true };
});
await dispatchWithContext({ context: createContext() });
expect(answerDraftStream.update).toHaveBeenCalledWith("Choose");
expect(mockCallArg(editMessageTelegram)).toBe(123);
expect(mockCallArg(editMessageTelegram, 0, 1)).toBe(2001);
expect(mockCallArg(editMessageTelegram, 0, 2)).toBe("Choose");
expectRecordFields(mockCallArg(editMessageTelegram, 0, 3), { buttons });
expect(deliverReplies).not.toHaveBeenCalled();
});
it("streams interactive buttons into the same message", async () => {
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver(
{
text: "Choose",
interactive: {
blocks: [{ type: "buttons", buttons: [{ label: "OK", value: "ok" }] }],
},
},
{ kind: "final" },
);
return { queuedFinal: true };
});
await dispatchWithContext({ context: createContext() });
expect(answerDraftStream.update).toHaveBeenCalledWith("Choose");
expectRecordFields(mockCallArg(editMessageTelegram, 0, 3), {
buttons: [[{ text: "OK", callback_data: "ok" }]],
});
expect(deliverReplies).not.toHaveBeenCalled();
});
it("streams reasoning and answer text on separate lanes", async () => {
const { answerDraftStream, reasoningDraftStream } = setupDraftStreams({
answerMessageId: 2001,
reasoningMessageId: 3001,
});
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
async ({ dispatcherOptions, replyOptions }) => {
await replyOptions?.onReasoningStream?.({ text: "Thinking" });
await dispatcherOptions.deliver({ text: "Answer" }, { kind: "final" });
return { queuedFinal: true };
},
);
await dispatchWithContext({ context: createReasoningStreamContext() });
expect(reasoningDraftStream.update).toHaveBeenCalledWith("🧠 _Thinking_");
expect(answerDraftStream.update).toHaveBeenCalledWith("Answer");
expect(deliverReplies).not.toHaveBeenCalled();
});
it("emits final hooks when a buffered answer flushes after reasoning delivery", async () => {
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
loadSessionStore.mockReturnValue({
s1: { reasoningLevel: "stream", sessionId: "reasoning-session" },
});
readLatestAssistantTextByIdentity.mockResolvedValue({
id: "assistant-buffered-final",
text: "Buffered answer",
timestamp: Date.now() + 1_000,
});
deliverReplies
.mockResolvedValueOnce({ delivered: false })
.mockResolvedValueOnce({ delivered: true });
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
await dispatcherOptions.deliver(
{ text: "first attempt", isReasoning: true },
{ kind: "block" },
);
await dispatcherOptions.deliver({ text: "Buffered answer" }, { kind: "final" });
await dispatcherOptions.deliver(
{ text: "second attempt", isReasoning: true },
{ kind: "block" },
);
return { queuedFinal: true };
});
await dispatchWithContext({
context: createContext({
ctxPayload: { SessionKey: "s1" } as unknown as TelegramMessageContext["ctxPayload"],
}),
});
expect(deliverReplies).toHaveBeenCalledTimes(2);
expect(answerDraftStream.update).toHaveBeenCalledWith("Buffered answer");
expectRecordFields(mockCallArg(emitInternalMessageSentHook), {
content: "Buffered answer",
messageId: 2001,
});
expectRecordFields(mockCallArg(recordOutboundMessageForPromptContext), {
messageId: 2001,
text: "Buffered answer",
promptContextProjection: {
transcriptMessageId: "assistant-buffered-final",
partIndex: 0,
finalPart: true,
},
});
await vi.waitFor(() => expect(appendAssistantMirrorMessageByIdentity).toHaveBeenCalledOnce());
});
it("preserves forum topic message_thread_id across streamed reasoning and final answer", async () => {
const { answerDraftStream, reasoningDraftStream } = setupDraftStreams({
answerMessageId: 2001,
reasoningMessageId: 3001,
});
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
async ({ dispatcherOptions, replyOptions }) => {
await replyOptions?.onReasoningStream?.({ text: "Thinking" });
await dispatcherOptions.deliver({ text: "Answer" }, { kind: "final" });
return { queuedFinal: true };
},
);
await dispatchWithContext({ context: createReasoningForumTopicContext() });
expect(reasoningDraftStream.update).toHaveBeenCalledWith("🧠 _Thinking_");
expect(answerDraftStream.update).toHaveBeenCalledWith("Answer");
expect(answerDraftStream.stop).toHaveBeenCalled();
expect(deliverReplies).not.toHaveBeenCalled();
expectRecordFields(mockCallArg(emitInternalMessageSentHook), {
content: "Answer",
messageId: 2001,
});
expectRecordFields(mockCallArg(recordOutboundMessageForPromptContext), {
chatId: "-100123",
messageId: 2001,
text: "Answer",
messageThreadId: 88,
});
expectDraftStreamParams({ thread: { id: 88, scope: "forum" } });
expectRecordFields(mockCallArg(createTelegramDraftStream, 1), {
thread: { id: 88, scope: "forum" },
});
});
it("replaces reasoning snapshots on the reasoning lane", async () => {
const { reasoningDraftStream } = setupDraftStreams({
answerMessageId: 2001,
reasoningMessageId: 3001,
});
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
const onReasoningStream = replyOptions?.onReasoningStream as
| ((payload: {
text?: string;
delta?: string;
isReasoningSnapshot?: boolean;
}) => Promise | void)
| undefined;
await onReasoningStream?.({
text: "Checking",
delta: "Checking",
isReasoningSnapshot: true,
});
await onReasoningStream?.({
text: "Reading\n\nChecking",
delta: "Reading\n\nChecking",
isReasoningSnapshot: true,
});
return { queuedFinal: false };
});
await dispatchWithContext({ context: createReasoningStreamContext() });
expect(reasoningDraftStream.update).toHaveBeenLastCalledWith("🧠 _Reading_\n\n_Checking_");
const updates = reasoningDraftStream.update.mock.calls.map((call) => call[0]);
expect(updates.join("\n")).not.toContain("CheckingReading");
});
it("repositions split reasoning before deleting the prior preview", async () => {
const answerDraftStream = createDraftStream(2001);
const reasoningDraftStream = createSequencedDraftStream(3001);
createTelegramDraftStream
.mockImplementationOnce(() => answerDraftStream)
.mockImplementationOnce(() => reasoningDraftStream);
let replacementMessageId: number | undefined;
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onReasoningStream?.({ text: "First thought" });
await replyOptions?.onReasoningEnd?.();
await replyOptions?.onReasoningStream?.({ text: "Second thought" });
replacementMessageId = reasoningDraftStream.messageId();
return { queuedFinal: false };
});
await dispatchWithContext({ context: createReasoningStreamContext() });
expect(reasoningDraftStream.update).toHaveBeenNthCalledWith(1, "🧠 _First thought_");
expect(reasoningDraftStream.update).toHaveBeenNthCalledWith(2, "🧠 _Second thought_");
expect(reasoningDraftStream.rotateToNewMessageDeferringDelete).toHaveBeenCalledTimes(1);
expect(reasoningDraftStream.forceNewMessage).not.toHaveBeenCalled();
expect(reasoningDraftStream.clear).toHaveBeenCalledTimes(1);
expect(
requireInvocationOrder(
reasoningDraftStream.rotateToNewMessageDeferringDelete,
0,
"first deferred reasoning draft rotation",
),
).toBeLessThan(
requireInvocationOrder(reasoningDraftStream.update, 1, "second reasoning draft update"),
);
expect(
requireInvocationOrder(reasoningDraftStream.update, 1, "second reasoning draft update"),
).toBeLessThan(
requireInvocationOrder(reasoningDraftStream.clear, 0, "first reasoning draft clear"),
);
expect(replacementMessageId).toBe(3002);
});
it("streams reasoning from configured defaults", async () => {
const { answerDraftStream, reasoningDraftStream } = setupDraftStreams({
answerMessageId: 2001,
reasoningMessageId: 3001,
});
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
async ({ dispatcherOptions, replyOptions }) => {
await replyOptions?.onReasoningStream?.({ text: "Thinking" });
await dispatcherOptions.deliver({ text: "Answer" }, { kind: "final" });
return { queuedFinal: true };
},
);
await dispatchWithContext({
context: createReasoningDefaultContext(),
cfg: {
agents: {
defaults: { reasoningDefault: "off" },
list: [{ id: "Ops", reasoningDefault: "stream" }],
},
},
});
expect(reasoningDraftStream.update).toHaveBeenCalledWith("🧠 _Thinking_");
expect(answerDraftStream.update).toHaveBeenCalledWith("Answer");
});
it("keeps reasoning draft labels static while the reasoning lane is active", async () => {
const { reasoningDraftStream } = setupDraftStreams({
answerMessageId: 2001,
reasoningMessageId: 3001,
});
let finishRun: (() => void) | undefined;
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
await replyOptions?.onReasoningStream?.({ text: "Thinking" });
await new Promise((resolve) => {
finishRun = resolve;
});
return { queuedFinal: false };
});
const run = dispatchWithContext({ context: createReasoningStreamContext() });
await vi.waitFor(() =>
expect(reasoningDraftStream.update).toHaveBeenCalledWith("🧠 _Thinking_"),
);
// Durable thoughts render behind the 🧠 marker; the literal "Thinking"
// header (and its streaming dot-variants) must never leak back into a lane.
expect(reasoningDraftStream.update).not.toHaveBeenCalledWith("Thinking\n\n_Thinking_");
expect(reasoningDraftStream.update).not.toHaveBeenCalledWith("Thinking.\n\n_Thinking_");
expect(reasoningDraftStream.update).not.toHaveBeenCalledWith("Thinking...\n\n_Thinking_");
finishRun?.();
await run;
});
});