From 530df255b2e8b47b2aa9e02d574ce28ed450fb63 Mon Sep 17 00:00:00 2001 From: fancyboi999 <135568692+fancyboi999@users.noreply.github.com> Date: Thu, 21 May 2026 16:26:25 +0800 Subject: [PATCH] fix(frontend): drop dead identity ternary and add opt-in export tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on the previous export commit: 1. Removed the no-op `typeof msg.content === "string" ? msg.content : msg.content` expression in `formatThreadAsJSON`. Both branches returned the same value; the message content now flows through unchanged whether it is a string or the rich `MessageContent[]` shape (LangChain JSON-serialises the array structure correctly already). 2. Expanded the JSDoc on `ExportOptions` to make it clearer that the four flags are not currently wired to any UI control — callers wanting a debug export must build the options object explicitly. The default behaviour continues to match the explicit prescription in bytedance/deer-flow#3107 BUG-006. 3. Added opt-in coverage. The previous tests only exercised the `options = {}` default path; the new cases verify each flag flips the corresponding payload back into the export so a future debug-export surface does not silently break the contract. Refs: bytedance/deer-flow#3107 (BUG-006) --- frontend/src/core/threads/export.ts | 15 ++--- .../tests/unit/core/threads/export.test.ts | 60 +++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/frontend/src/core/threads/export.ts b/frontend/src/core/threads/export.ts index 5d40eeab5..92c4ae85f 100644 --- a/frontend/src/core/threads/export.ts +++ b/frontend/src/core/threads/export.ts @@ -15,12 +15,13 @@ import { titleOfThread } from "./utils"; /** * Optional debug switches for advanced exports. * - * Bytedance/deer-flow issue #3107 BUG-006: by default, the user-facing chat - * export must include only the visible transcript. Internal payloads — - * `hide_from_ui` messages, reasoning content, tool calls, and tool result - * messages — stay out unless the caller explicitly opts in. There is no UI - * surface for this today; the flags exist so a future "debug export" can - * reuse the same formatter instead of forking it. + * Bytedance/deer-flow issue #3107 BUG-006 explicitly prescribes that the + * default export includes only the user-visible transcript and excludes + * thinking/reasoning content, tool calls, tool results, hidden messages, + * memory injection, and `` payloads. These options let a + * future "debug export" surface re-include any of those categories without + * forking the formatter. They are not currently wired to any UI control — + * callers that want them must construct the options object explicitly. */ export interface ExportOptions { includeReasoning?: boolean; @@ -134,7 +135,7 @@ export function formatThreadAsJSON( messages: visibleMessages(messages, options).map((msg) => ({ type: msg.type, id: msg.id, - content: typeof msg.content === "string" ? msg.content : msg.content, + content: msg.content, ...(options.includeToolCalls && msg.type === "ai" && msg.tool_calls?.length diff --git a/frontend/tests/unit/core/threads/export.test.ts b/frontend/tests/unit/core/threads/export.test.ts index de755387d..fd59ff4cb 100644 --- a/frontend/tests/unit/core/threads/export.test.ts +++ b/frontend/tests/unit/core/threads/export.test.ts @@ -104,6 +104,66 @@ describe("formatThreadAsMarkdown", () => { }); }); +describe("formatThreadAsMarkdown opt-in flags", () => { + it("emits reasoning when includeReasoning is true", () => { + const message = ai("final answer", { + additional_kwargs: { + reasoning_content: "step-by-step chain of thought", + }, + } as Partial); + const md = formatThreadAsMarkdown(makeThread(), [message], { + includeReasoning: true, + }); + expect(md).toContain("step-by-step chain of thought"); + expect(md).toContain("Thinking"); + }); + + it("emits tool call rows when includeToolCalls is true", () => { + const message = ai("ok", { + tool_calls: [{ id: "1", name: "task", args: { description: "do work" } }], + } as Partial); + const md = formatThreadAsMarkdown(makeThread(), [message], { + includeToolCalls: true, + }); + expect(md).toContain("**Tool:**"); + expect(md).toContain("`task`"); + }); + + it("keeps hidden messages when includeHidden is true", () => { + const hidden = human("internal reminder", { + additional_kwargs: { hide_from_ui: true }, + } as Partial); + const md = formatThreadAsMarkdown(makeThread(), [hidden], { + includeHidden: true, + }); + expect(md).toContain("internal reminder"); + }); +}); + +describe("formatThreadAsJSON opt-in flags", () => { + it("emits tool_calls field when includeToolCalls is true", () => { + const message = ai("ok", { + tool_calls: [{ id: "1", name: "task", args: { description: "x" } }], + } as Partial); + const raw = formatThreadAsJSON(makeThread(), [message], { + includeToolCalls: true, + }); + expect(raw).toContain("tool_calls"); + expect(raw).toContain('"task"'); + }); + + it("keeps tool messages when includeToolMessages is true", () => { + const raw = formatThreadAsJSON( + makeThread(), + [toolMsg("Task Succeeded. Result: keep me")], + { includeToolMessages: true }, + ); + const parsed = JSON.parse(raw) as { messages: { type: string }[] }; + expect(parsed.messages.some((m) => m.type === "tool")).toBe(true); + expect(raw).toContain("keep me"); + }); +}); + describe("formatThreadAsJSON", () => { it("strips hidden messages, tool messages, reasoning, and tool calls", () => { const messages = [