






















@@ -86,13 +86,17 @@ vi.mock("./streaming-card.js", () => {
8686};
8787});
888889-import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
89+import {
90+clearFeishuStreamingStartBackoffForTests,
91+createFeishuReplyDispatcher,
92+} from "./reply-dispatcher.js";
90939194describe("createFeishuReplyDispatcher streaming behavior", () => {
9295type ReplyDispatcherArgs = Parameters<typeof createFeishuReplyDispatcher>[0];
93969497beforeEach(() => {
9598vi.clearAllMocks();
99+clearFeishuStreamingStartBackoffForTests();
96100streamingInstances.length = 0;
97101sendMediaFeishuMock.mockResolvedValue(undefined);
98102sendStructuredCardFeishuMock.mockResolvedValue(undefined);
@@ -731,9 +735,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
731735);
732736});
733737734-it("recovers streaming after start() throws (HTTP 400)", async () => {
738+it("backs off streaming retries after start() throws (HTTP 400)", async () => {
735739const errorMock = vi.fn();
736740let shouldFailStart = true;
741+const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_000);
737742738743// Intercept streaming instance creation to make first start() reject
739744const origPush = streamingInstances.push.bind(streamingInstances);
@@ -758,22 +763,33 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
758763const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
759764760765// First deliver with markdown triggers startStreaming - which will fail
761-await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "block" });
766+await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
762767763768// Wait for the async error to propagate
764769await vi.waitFor(() => {
765770expect(errorMock).toHaveBeenCalledWith(expect.stringContaining("streaming start failed"));
766771});
772+expect(streamingInstances).toHaveLength(1);
773+expect(sendStructuredCardFeishuMock).toHaveBeenCalledTimes(1);
767774768-// Second deliver should create a NEW streaming session (not stuck)
775+// Immediate next markdown reply should skip a new streaming start and
776+// fall back directly to a normal card instead of paying the 400 latency.
769777await options.deliver({ text: "```ts\nconst y = 2\n```" }, { kind: "final" });
770778771-// Two instances created: first failed, second succeeded and closed
779+expect(streamingInstances).toHaveLength(1);
780+expect(sendStructuredCardFeishuMock).toHaveBeenCalledTimes(2);
781+782+// After the short backoff expires, retry streaming so fixed permissions
783+// or transient Feishu failures recover without a process restart.
784+nowSpy.mockReturnValue(62_000);
785+await options.deliver({ text: "```ts\nconst z = 3\n```" }, { kind: "final" });
786+772787expect(streamingInstances).toHaveLength(2);
773788expect(streamingInstances[1].start).toHaveBeenCalled();
774789expect(streamingInstances[1].close).toHaveBeenCalled();
775790} finally {
776791streamingInstances.push = origPush;
792+nowSpy.mockRestore();
777793}
778794});
779795});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。