

























@@ -3573,6 +3573,150 @@ describe("dispatchTelegramMessage draft streaming", () => {
35733573await sidePromise;
35743574});
357535753576+it("does not drop the first chunk of a long final after a generic lane rotation", async () => {
3577+const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
3578+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
3579+async ({ dispatcherOptions, replyOptions }) => {
3580+await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
3581+await dispatcherOptions.deliver(
3582+{ text: "A".repeat(4000) + "B".repeat(4000) },
3583+{ kind: "final" },
3584+);
3585+return { queuedFinal: true };
3586+},
3587+);
3588+3589+await dispatchWithContext({
3590+context: createContext(),
3591+textLimit: 4000,
3592+});
3593+3594+expect(answerDraftStream.update).toHaveBeenCalledWith("A".repeat(4000));
3595+});
3596+3597+it("does not suppress text-only blocks as delivered when answer draft is inactive", async () => {
3598+setupDraftStreams({ answerMessageId: 2001 });
3599+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
3600+await dispatcherOptions.deliver({ text: "forced block" }, { kind: "block" });
3601+await dispatcherOptions.deliver({ text: "final text" }, { kind: "final" });
3602+return { queuedFinal: true };
3603+});
3604+3605+await dispatchWithContext({
3606+context: createContext(),
3607+streamMode: "partial",
3608+telegramCfg: {
3609+streaming: { mode: "partial", block: { enabled: true } },
3610+} satisfies Parameters<typeof dispatchTelegramMessage>[0]["telegramCfg"],
3611+});
3612+3613+const deliveredTexts = deliverReplies.mock.calls.flatMap((call) =>
3614+((call[0] as { replies?: Array<{ text?: string }> }).replies ?? []).map(
3615+(reply) => reply.text,
3616+),
3617+);
3618+expect(deliveredTexts).toContain("forced block");
3619+});
3620+3621+it("does not suppress text-only blocks after a tool-progress draft", async () => {
3622+const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
3623+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
3624+async ({ dispatcherOptions, replyOptions }) => {
3625+await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
3626+await dispatcherOptions.deliver({ text: "block after progress" }, { kind: "block" });
3627+return { queuedFinal: true };
3628+},
3629+);
3630+3631+await dispatchWithContext({
3632+context: createContext(),
3633+streamMode: "partial",
3634+telegramCfg: { streaming: { mode: "partial" } },
3635+});
3636+3637+expect(mockCallArg(answerDraftStream.update)).toContain("Exec");
3638+expect(answerDraftStream.update).toHaveBeenLastCalledWith("block after progress");
3639+});
3640+3641+it("does not suppress button-bearing blocks after answer streaming starts", async () => {
3642+const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
3643+const buttons = [[{ text: "OK", callback_data: "ok" }]];
3644+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
3645+async ({ dispatcherOptions, replyOptions }) => {
3646+await replyOptions?.onPartialReply?.({ text: "partial answer" });
3647+await dispatcherOptions.deliver(
3648+{ text: "choose now", channelData: { telegram: { buttons } } },
3649+{ kind: "block" },
3650+);
3651+return { queuedFinal: true };
3652+},
3653+);
3654+3655+await dispatchWithContext({
3656+context: createContext(),
3657+streamMode: "partial",
3658+telegramCfg: { streaming: { mode: "partial" } },
3659+});
3660+3661+expect(answerDraftStream.update).toHaveBeenLastCalledWith("choose now");
3662+expectRecordFields(mockCallArg(editMessageTelegram, 0, 3), { buttons });
3663+});
3664+3665+it("finalizes a duplicate text-only block when no final follows", async () => {
3666+const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
3667+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
3668+async ({ dispatcherOptions, replyOptions }) => {
3669+await replyOptions?.onPartialReply?.({ text: "partial answer" });
3670+await dispatcherOptions.deliver({ text: "partial answer" }, { kind: "block" });
3671+return { queuedFinal: false };
3672+},
3673+);
3674+3675+await dispatchWithContext({
3676+context: createContext(),
3677+streamMode: "partial",
3678+telegramCfg: { streaming: { mode: "partial" } },
3679+});
3680+3681+expect(answerDraftStream.stop).toHaveBeenCalled();
3682+expect(answerDraftStream.clear).not.toHaveBeenCalled();
3683+expectRecordFields(mockCallArg(emitInternalMessageSentHook), {
3684+content: "partial answer",
3685+messageId: 2001,
3686+});
3687+expectRecordFields(mockCallArg(recordOutboundMessageForPromptContext), {
3688+text: "partial answer",
3689+messageId: 2001,
3690+});
3691+});
3692+3693+it("materializes a pending duplicate text-only block before finalizing it", async () => {
3694+const { answerDraftStream } = setupDraftStreams();
3695+answerDraftStream.stop.mockImplementation(async () => {
3696+answerDraftStream.setMessageId(2001);
3697+});
3698+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
3699+async ({ dispatcherOptions, replyOptions }) => {
3700+await replyOptions?.onPartialReply?.({ text: "pending answer" });
3701+await dispatcherOptions.deliver({ text: "pending answer" }, { kind: "block" });
3702+return { queuedFinal: false };
3703+},
3704+);
3705+3706+await dispatchWithContext({
3707+context: createContext(),
3708+streamMode: "partial",
3709+telegramCfg: { streaming: { mode: "partial" } },
3710+});
3711+3712+expect(answerDraftStream.stop).toHaveBeenCalled();
3713+expect(answerDraftStream.clear).not.toHaveBeenCalled();
3714+expectRecordFields(mockCallArg(emitInternalMessageSentHook), {
3715+content: "pending answer",
3716+messageId: 2001,
3717+});
3718+});
3719+35763720it("keeps queued room events abortable after their source dispatch returns", async () => {
35773721const historyKey = "telegram:group:-100123";
35783722const groupHistories = new Map([[historyKey, []]]);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。