From fdfb79530ef89ef424e3780b7e8a7da17a085560 Mon Sep 17 00:00:00 2001 From: Lluis Agusti Date: Thu, 16 Apr 2026 16:54:16 +0700 Subject: [PATCH] test(frontend/copilot): add tests for splitReasoningAndResponse and isReasoningToolPart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers: - isReasoningToolPart returns true for search/reasoning tools, false for action tools - splitReasoningAndResponse: all text (no tools) → all response - splitReasoningAndResponse: text + reasoning tools + text → proper split - splitReasoningAndResponse: text + action tools + text → all response (no split) - splitReasoningAndResponse: mixed reasoning + action tools → only reasoning triggers split - splitReasoningAndResponse: reasoning tools with no text after → all response - splitReasoningAndResponse: action tool after response text stays visible Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ChatMessagesContainer/helpers.test.ts | 153 +++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.test.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.test.ts index 831894d57b..d44976b953 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.test.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.test.ts @@ -1,5 +1,29 @@ import { describe, expect, it } from "vitest"; -import { extractWorkspaceArtifacts, filePartToArtifactRef } from "./helpers"; +import { + extractWorkspaceArtifacts, + filePartToArtifactRef, + isReasoningToolPart, + splitReasoningAndResponse, +} from "./helpers"; +import type { MessagePart } from "./helpers"; + +function textPart(text: string): MessagePart { + return { type: "text", text } as MessagePart; +} + +function toolPart( + toolName: string, + state: string = "output-available", +): MessagePart { + return { + type: `tool-${toolName}`, + state, + toolCallId: `call-${toolName}`, + toolName, + args: {}, + output: "{}", + } as unknown as MessagePart; +} describe("extractWorkspaceArtifacts", () => { it("extracts a single workspace:// link with its markdown title", () => { @@ -101,3 +125,130 @@ describe("filePartToArtifactRef", () => { expect(overridden?.origin).toBe("agent"); }); }); + +describe("isReasoningToolPart", () => { + it("returns true for reasoning/search tools", () => { + const reasoningTools = [ + "find_block", + "find_agent", + "find_library_agent", + "search_docs", + "get_doc_page", + "search_feature_requests", + "ask_question", + ]; + for (const name of reasoningTools) { + expect(isReasoningToolPart(toolPart(name))).toBe(true); + } + }); + + it("returns false for action tools", () => { + const actionTools = [ + "run_block", + "run_agent", + "create_agent", + "edit_agent", + "run_mcp_tool", + "schedule_agent", + "continue_run_block", + ]; + for (const name of actionTools) { + expect(isReasoningToolPart(toolPart(name))).toBe(false); + } + }); + + it("returns false for text parts", () => { + expect(isReasoningToolPart(textPart("hello"))).toBe(false); + }); +}); + +describe("splitReasoningAndResponse", () => { + it("returns all parts as response when there are no tools", () => { + const parts = [textPart("Hello"), textPart("World")]; + const result = splitReasoningAndResponse(parts); + expect(result.reasoning).toEqual([]); + expect(result.response).toEqual(parts); + }); + + it("splits on reasoning tools — text before goes to reasoning", () => { + const parts = [ + textPart("Let me search..."), + toolPart("find_block"), + textPart("Here is your answer"), + ]; + const result = splitReasoningAndResponse(parts); + expect(result.reasoning).toHaveLength(2); + expect(result.response).toHaveLength(1); + expect((result.response[0] as { text: string }).text).toBe( + "Here is your answer", + ); + }); + + it("does NOT split on action tools — response before run_block stays visible", () => { + const parts = [ + textPart("Here is my answer"), + toolPart("run_block"), + textPart("Block finished"), + ]; + const result = splitReasoningAndResponse(parts); + expect(result.reasoning).toEqual([]); + expect(result.response).toEqual(parts); + }); + + it("splits only on reasoning tools when both reasoning and action tools are present", () => { + const parts = [ + textPart("Planning..."), + toolPart("search_docs"), + textPart("Found it. Running now."), + toolPart("run_block"), + textPart("Done!"), + ]; + const result = splitReasoningAndResponse(parts); + expect(result.reasoning).toHaveLength(2); + expect(result.response).toHaveLength(3); + expect((result.response[0] as { text: string }).text).toBe( + "Found it. Running now.", + ); + }); + + it("returns all as response when reasoning tools have no text after them", () => { + const parts = [ + textPart("Hello"), + toolPart("find_agent"), + toolPart("run_block"), + ]; + const result = splitReasoningAndResponse(parts); + expect(result.reasoning).toEqual([]); + expect(result.response).toEqual(parts); + }); + + it("handles multiple reasoning tools correctly", () => { + const parts = [ + textPart("Searching..."), + toolPart("find_block"), + textPart("Found one, searching more..."), + toolPart("search_docs"), + textPart("Here are the results"), + ]; + const result = splitReasoningAndResponse(parts); + expect(result.reasoning).toHaveLength(4); + expect(result.response).toHaveLength(1); + expect((result.response[0] as { text: string }).text).toBe( + "Here are the results", + ); + }); + + it("handles action tool after response text without hiding the response", () => { + const parts = [ + toolPart("find_block"), + textPart("I found it! Let me run it."), + toolPart("run_agent"), + ]; + const result = splitReasoningAndResponse(parts); + expect(result.reasoning).toHaveLength(1); + expect(result.response).toHaveLength(2); + expect((result.response[0] as { text: string }).text).toBe( + "I found it! Let me run it.", + ); + }); +});