





















@@ -491,6 +491,64 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
491491});
492492});
493493494+it("preserves previous generation blocks when partial snapshots reset after tools", async () => {
495+resolveFeishuAccountMock.mockReturnValue({
496+accountId: "main",
497+appId: "app_id",
498+appSecret: "app_secret",
499+domain: "feishu",
500+config: {
501+renderMode: "card",
502+streaming: true,
503+},
504+});
505+506+const { result, options } = createDispatcherHarness({
507+runtime: createRuntimeLogger(),
508+});
509+await options.onReplyStart?.();
510+result.replyOptions.onPartialReply?.({
511+text: "Preparing the lookup plan with enough text to count as one block.",
512+});
513+result.replyOptions.onPartialReply?.({ text: "Found" });
514+result.replyOptions.onPartialReply?.({ text: "Found the answer." });
515+await options.onIdle?.();
516+517+expect(streamingInstances).toHaveLength(1);
518+expect(streamingInstances[0].close).toHaveBeenCalledWith(
519+"Preparing the lookup plan with enough text to count as one block.Found the answer.",
520+{
521+note: "Agent: agent",
522+},
523+);
524+});
525+526+it("strips reasoning tags from streamed partial snapshots", async () => {
527+resolveFeishuAccountMock.mockReturnValue({
528+accountId: "main",
529+appId: "app_id",
530+appSecret: "app_secret",
531+domain: "feishu",
532+config: {
533+renderMode: "card",
534+streaming: true,
535+},
536+});
537+538+const { result, options } = createDispatcherHarness({
539+runtime: createRuntimeLogger(),
540+});
541+await options.onReplyStart?.();
542+result.replyOptions.onPartialReply?.({
543+text: "<thinking>private chain of thought</thinking>\nvisible answer",
544+});
545+await options.onIdle?.();
546+547+expect(streamingInstances[0].close).toHaveBeenCalledWith("visible answer", {
548+note: "Agent: agent",
549+});
550+});
551+494552it("sends media-only payloads as attachments", async () => {
495553const { options } = createDispatcherHarness();
496554await options.deliver({ mediaUrl: "https://example.com/a.png" }, { kind: "final" });
@@ -757,7 +815,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
757815);
758816});
759817760-it("disables streaming for thread replies and keeps reply metadata", async () => {
818+it("uses streaming cards for thread replies and keeps topic metadata", async () => {
761819const { options } = createDispatcherHarness({
762820runtime: createRuntimeLogger(),
763821replyToMessageId: "om_msg",
@@ -767,13 +825,127 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
767825});
768826await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
769827770-expect(streamingInstances).toHaveLength(0);
771-expect(sendStructuredCardFeishuMock).toHaveBeenCalledWith(
828+expect(streamingInstances).toHaveLength(1);
829+expect(streamingInstances[0].start).toHaveBeenCalledWith(
830+"oc_chat",
831+"chat_id",
772832expect.objectContaining({
773833replyToMessageId: "om_msg",
774834replyInThread: true,
835+rootId: "om_root_topic",
775836}),
776837);
838+expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
839+});
840+841+it("omits the generic main header from streaming and static cards", async () => {
842+resolveFeishuAccountMock.mockReturnValue({
843+accountId: "main",
844+appId: "app_id",
845+appSecret: "app_secret",
846+domain: "feishu",
847+config: {
848+renderMode: "card",
849+streaming: true,
850+},
851+});
852+853+const { options } = createDispatcherHarness({
854+agentId: "main",
855+runtime: createRuntimeLogger(),
856+});
857+await options.deliver({ text: "streamed card" }, { kind: "final" });
858+await options.onIdle?.();
859+860+expect(streamingInstances[0].start).toHaveBeenCalledWith(
861+"oc_chat",
862+"chat_id",
863+expect.objectContaining({
864+header: undefined,
865+}),
866+);
867+868+resolveFeishuAccountMock.mockReturnValue({
869+accountId: "main",
870+appId: "app_id",
871+appSecret: "app_secret",
872+domain: "feishu",
873+config: {
874+renderMode: "card",
875+streaming: false,
876+},
877+});
878+879+const { options: staticOptions } = createDispatcherHarness({
880+agentId: "main",
881+runtime: createRuntimeLogger(),
882+});
883+await staticOptions.deliver({ text: "static card" }, { kind: "final" });
884+885+expect(sendStructuredCardFeishuMock).toHaveBeenCalledWith(
886+expect.objectContaining({
887+header: undefined,
888+}),
889+);
890+});
891+892+it("shows transient tool status on streaming cards but omits it from the final close", async () => {
893+resolveFeishuAccountMock.mockReturnValue({
894+accountId: "main",
895+appId: "app_id",
896+appSecret: "app_secret",
897+domain: "feishu",
898+config: {
899+renderMode: "card",
900+streaming: true,
901+},
902+});
903+904+const { result, options } = createDispatcherHarness({
905+runtime: createRuntimeLogger(),
906+});
907+await options.onReplyStart?.();
908+result.replyOptions.onToolStart?.({ name: "web_search" });
909+result.replyOptions.onPartialReply?.({ text: "final answer" });
910+await options.onIdle?.();
911+912+const updateTexts = streamingInstances[0].update.mock.calls.map((call: unknown[]) =>
913+typeof call[0] === "string" ? call[0] : "",
914+);
915+expect(updateTexts.some((text) => text.includes("Using: web_search"))).toBe(true);
916+expect(streamingInstances[0].close).toHaveBeenCalledWith("final answer", {
917+note: "Agent: agent",
918+});
919+});
920+921+it("cleans streaming state even when close throws", async () => {
922+const origPush = streamingInstances.push.bind(streamingInstances);
923+streamingInstances.push = (...args: StreamingSessionStub[]) => {
924+if (args.length > 0 && streamingInstances.length === 0) {
925+args[0].close = vi.fn(async () => {
926+args[0].active = false;
927+throw new Error("close failed");
928+});
929+}
930+return origPush(...args);
931+};
932+933+try {
934+const { options } = createDispatcherHarness({
935+runtime: createRuntimeLogger(),
936+});
937+await options.deliver({ text: "```md\nfirst\n```" }, { kind: "final" });
938+await expect(options.onIdle?.()).rejects.toThrow("close failed");
939+await options.deliver({ text: "```md\nsecond\n```" }, { kind: "final" });
940+await options.onIdle?.();
941+942+expect(streamingInstances).toHaveLength(2);
943+expect(streamingInstances[1].close).toHaveBeenCalledWith("```md\nsecond\n```", {
944+note: "Agent: agent",
945+});
946+} finally {
947+streamingInstances.push = origPush;
948+}
777949});
778950779951it("passes replyInThread to media attachments", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。