




















@@ -159,6 +159,51 @@ describe("createMSTeamsReplyDispatcher", () => {
159159return lastContextSendActivity;
160160}
161161162+type DispatcherOptions = {
163+onReplyStart?: () => Promise<void> | void;
164+deliver: (payload: { text: string }) => Promise<void> | void;
165+};
166+167+type PipelineArgs = {
168+typing?: {
169+keepaliveIntervalMs?: number;
170+maxDurationMs?: number;
171+start?: () => Promise<void>;
172+};
173+};
174+175+function dispatcherOptions(): DispatcherOptions {
176+const [call] = createReplyDispatcherWithTypingMock.mock.calls;
177+if (!call) {
178+throw new Error("expected reply dispatcher factory call");
179+}
180+return call[0] as DispatcherOptions;
181+}
182+183+function pipelineArgs(): PipelineArgs {
184+const [call] = createChannelMessageReplyPipelineMock.mock.calls;
185+if (!call) {
186+throw new Error("expected reply pipeline factory call");
187+}
188+return call[0] as PipelineArgs;
189+}
190+191+function pipelineTypingStart(): () => Promise<void> {
192+const sendTyping = pipelineArgs().typing?.start;
193+if (typeof sendTyping !== "function") {
194+throw new Error("expected typing start callback");
195+}
196+return sendTyping;
197+}
198+199+function firstSystemEventCall(): [string, unknown] {
200+const [call] = enqueueSystemEventMock.mock.calls;
201+if (!call) {
202+throw new Error("expected system event call");
203+}
204+return call as [string, unknown];
205+}
206+162207async function triggerPartialReply(text: string): Promise<void> {
163208if (!lastCreatedDispatcher) {
164209throw new Error("createDispatcher must be called first");
@@ -168,7 +213,7 @@ describe("createMSTeamsReplyDispatcher", () => {
168213169214it("sends an informative status update once work expands in personal chats", async () => {
170215const dispatcher = createDispatcher("personal", { streaming: { mode: "progress" } });
171-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
216+const options = dispatcherOptions();
172217173218await options.onReplyStart?.();
174219await dispatcher.replyOptions.onToolStart?.({ name: "exec" });
@@ -180,7 +225,7 @@ describe("createMSTeamsReplyDispatcher", () => {
180225181226it("starts the typing keepalive in personal chats so the TurnContext survives long tool chains", async () => {
182227createDispatcher("personal");
183-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
228+const options = dispatcherOptions();
184229185230await options.onReplyStart?.();
186231@@ -192,7 +237,7 @@ describe("createMSTeamsReplyDispatcher", () => {
192237193238it("skips the typing keepalive in personal chats when typingIndicator=false", async () => {
194239createDispatcher("personal", { typingIndicator: false });
195-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
240+const options = dispatcherOptions();
196241197242await options.onReplyStart?.();
198243@@ -203,18 +248,17 @@ describe("createMSTeamsReplyDispatcher", () => {
203248it("passes a longer keepalive TTL so the loop survives long tool chains", () => {
204249createDispatcher("personal");
205250206-const pipelineArgs = createChannelMessageReplyPipelineMock.mock.calls[0]?.[0];
207-expect(pipelineArgs?.typing?.keepaliveIntervalMs).toBeGreaterThan(3_000);
208-expect(pipelineArgs?.typing?.keepaliveIntervalMs).toBeLessThanOrEqual(10_000);
251+const args = pipelineArgs();
252+expect(args.typing?.keepaliveIntervalMs).toBeGreaterThan(3_000);
253+expect(args.typing?.keepaliveIntervalMs).toBeLessThanOrEqual(10_000);
209254// Issue #59731 reports 60s+ tool chains — the default 60s TTL is too
210255// tight so the dispatcher passes its own generous ceiling.
211-expect(pipelineArgs?.typing?.maxDurationMs).toBeGreaterThanOrEqual(300_000);
256+expect(args.typing?.maxDurationMs).toBeGreaterThanOrEqual(300_000);
212257});
213258214259it("allows typing keepalive sends before any stream tokens arrive", async () => {
215260createDispatcher("personal");
216-const pipelineArgs = createChannelMessageReplyPipelineMock.mock.calls[0]?.[0];
217-const sendTyping = pipelineArgs?.typing?.start as () => Promise<void>;
261+const sendTyping = pipelineTypingStart();
218262219263// No onPartialReply has been called yet, so the stream is not active.
220264// The typing keepalive should be allowed to warm the TurnContext.
@@ -226,8 +270,7 @@ describe("createMSTeamsReplyDispatcher", () => {
226270227271it("suppresses typing keepalive sends while the stream card is actively chunking", async () => {
228272createDispatcher("personal");
229-const pipelineArgs = createChannelMessageReplyPipelineMock.mock.calls[0]?.[0];
230-const sendTyping = pipelineArgs?.typing?.start as () => Promise<void>;
273+const sendTyping = pipelineTypingStart();
231274232275// Simulate the stream actively receiving a partial chunk. While the
233276// stream card is live we do not want a plain "..." typing indicator
@@ -242,8 +285,7 @@ describe("createMSTeamsReplyDispatcher", () => {
242285243286it("resumes typing keepalive sends once the stream finalizes between tool rounds", async () => {
244287createDispatcher("personal");
245-const pipelineArgs = createChannelMessageReplyPipelineMock.mock.calls[0]?.[0];
246-const sendTyping = pipelineArgs?.typing?.start as () => Promise<void>;
288+const sendTyping = pipelineTypingStart();
247289248290// First segment: tokens flow, stream is active, typing is gated off.
249291await triggerPartialReply("first segment tokens");
@@ -271,8 +313,7 @@ describe("createMSTeamsReplyDispatcher", () => {
271313272314it("fires native typing in group chats (no stream) because the gate never applies", async () => {
273315createDispatcher("groupchat");
274-const pipelineArgs = createChannelMessageReplyPipelineMock.mock.calls[0]?.[0];
275-const sendTyping = pipelineArgs?.typing?.start as () => Promise<void>;
316+const sendTyping = pipelineTypingStart();
276317277318// In group chats we don't create a stream, so isStreamActive() always
278319// returns false and the typing indicator still fires normally.
@@ -284,8 +325,7 @@ describe("createMSTeamsReplyDispatcher", () => {
284325285326it("is a no-op for channel conversations (typing unsupported)", async () => {
286327createDispatcher("channel");
287-const pipelineArgs = createChannelMessageReplyPipelineMock.mock.calls[0]?.[0];
288-const sendTyping = pipelineArgs?.typing?.start as () => Promise<void>;
328+const sendTyping = pipelineTypingStart();
289329290330const contextSendActivity = getContextSendActivity();
291331contextSendActivity.mockClear();
@@ -297,7 +337,7 @@ describe("createMSTeamsReplyDispatcher", () => {
297337298338it("sends native typing indicator for channel conversations by default", async () => {
299339createDispatcher("channel");
300-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
340+const options = dispatcherOptions();
301341302342await options.onReplyStart?.();
303343@@ -307,7 +347,7 @@ describe("createMSTeamsReplyDispatcher", () => {
307347308348it("skips native typing indicator when typingIndicator=false", async () => {
309349createDispatcher("channel", { typingIndicator: false });
310-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
350+const options = dispatcherOptions();
311351312352await options.onReplyStart?.();
313353@@ -393,7 +433,7 @@ describe("createMSTeamsReplyDispatcher", () => {
393433sendMSTeamsMessagesMock.mockResolvedValue(["id-1"] as never);
394434395435const dispatcher = createDispatcher("personal", { streaming: { mode: "block" } });
396-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
436+const options = dispatcherOptions();
397437398438await options.deliver({ text: "block content" });
399439@@ -420,7 +460,7 @@ describe("createMSTeamsReplyDispatcher", () => {
420460sendMSTeamsMessagesMock.mockResolvedValue(["id-1"] as never);
421461422462createDispatcher("personal", { blockStreaming: true });
423-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
463+const options = dispatcherOptions();
424464425465// Call deliver — with blockStreaming enabled it should flush immediately
426466await options.deliver({ text: "block content" });
@@ -432,7 +472,7 @@ describe("createMSTeamsReplyDispatcher", () => {
432472renderReplyPayloadsToMessagesMock.mockReturnValue([{ content: "hello" }] as never);
433473434474createDispatcher("personal", { blockStreaming: false });
435-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
475+const options = dispatcherOptions();
436476437477await options.deliver({ text: "block content" });
438478@@ -455,14 +495,14 @@ describe("createMSTeamsReplyDispatcher", () => {
455495{ blockStreaming: false },
456496{ onSentMessageIds },
457497);
458-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
498+const options = dispatcherOptions();
459499460500await options.deliver({ text: "block content" });
461501await dispatcher.markDispatchIdle();
462502463503expect(onSentMessageIds).toHaveBeenCalledWith(["id-1"]);
464504expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
465-const [message, context] = enqueueSystemEventMock.mock.calls[0] ?? [];
505+const [message, context] = firstSystemEventCall();
466506expect(message).toContain("Microsoft Teams delivery failed");
467507expect(message).toContain("1 of 2 message blocks were not delivered");
468508expect(message).toContain("The user may not have received the full reply");
@@ -478,7 +518,7 @@ describe("createMSTeamsReplyDispatcher", () => {
478518sendMSTeamsMessagesMock.mockResolvedValue(["id-1"] as never);
479519480520const dispatcher = createDispatcher("personal", { blockStreaming: false });
481-const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
521+const options = dispatcherOptions();
482522483523await options.deliver({ text: "block content" });
484524await dispatcher.markDispatchIdle();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。