import type { AssistantMessage } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; import { extractAssistantText, formatReasoningMessage, promoteThinkingTagsToBlocks, stripDowngradedToolCallText, } from "./pi-embedded-utils.js"; function makeAssistantMessage( message: Omit & Partial>, ): AssistantMessage { return { api: "responses", provider: "openai", model: "gpt-5", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, stopReason: "stop", ...message, }; } describe("extractAssistantText", () => { it("strips tool-only Minimax invocation XML from text", () => { const cases = [ ` netstat -tlnp | grep 18789 `, ` test `, ]; for (const text of cases) { const msg = makeAssistantMessage({ role: "assistant", content: [{ type: "text", text }], timestamp: Date.now(), }); expect(extractAssistantText(msg)).toBe(""); } }); it("strips multiple tool invocations", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `Let me check that. /home/admin/test.txt `, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("Let me check that."); }); it("keeps invoke snippets without Minimax markers", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `Example:\n\nls\n`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe( `Example:\n\nls\n`, ); }); it("preserves normal text without tool invocations", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: "This is a normal response without any tool calls.", }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("This is a normal response without any tool calls."); }); it("sanitizes HTTP-ish error text only when stopReason is error", () => { const msg = makeAssistantMessage({ role: "assistant", stopReason: "error", errorMessage: "500 Internal Server Error", content: [{ type: "text", text: "500 Internal Server Error" }], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("HTTP 500: Internal Server Error"); }); it("does not rewrite normal text that references billing plans", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: "Firebase downgraded Chore Champ to the Spark plan; confirm whether billing should be re-enabled.", }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe( "Firebase downgraded Chore Champ to the Spark plan; confirm whether billing should be re-enabled.", ); }); it("strips Minimax tool invocations with extra attributes", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `Before\nls\n\nAfter`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("Before\nAfter"); }); it("strips minimax tool_call open and close tags", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: "StartInnerEnd", }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("StartInnerEnd"); }); it("ignores invoke blocks without minimax markers", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: "BeforeKeepAfter", }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("BeforeKeepAfter"); }); it("strips invoke blocks when minimax markers are present elsewhere", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: "BeforeDropAfter", }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("BeforeAfter"); }); it("strips invoke blocks with nested tags", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `A1B`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("AB"); }); it("strips tool XML mixed with regular content", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `I'll help you with that. ls -la Here are the results.`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("I'll help you with that.\nHere are the results."); }); it("handles multiple invoke blocks in one message", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `First check. file1.txt Second check. pwd Done.`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("First check.\nSecond check.\nDone."); }); it("handles stray closing tags without opening tags", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: "Some text here.More text.", }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("Some text here.More text."); }); it("handles multiple text blocks", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: "First block.", }, { type: "text", text: ` ls `, }, { type: "text", text: "Third block.", }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("First block.\nThird block."); }); it("strips downgraded Gemini tool call text representations", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `[Tool Call: exec (ID: toolu_vrtx_014w1P6B6w4V92v4VzG7Qk12)] Arguments: { "command": "git status", "timeout": 120000 }`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe(""); }); it("strips multiple downgraded tool calls", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `[Tool Call: read (ID: toolu_1)] Arguments: { "path": "/some/file.txt" } [Tool Call: exec (ID: toolu_2)] Arguments: { "command": "ls -la" }`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe(""); }); it("strips tool results for downgraded calls", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `[Tool Result for ID toolu_123] {"status": "ok", "data": "some result"}`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe(""); }); it("preserves text around downgraded tool calls", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `Let me check that for you. [Tool Call: browser (ID: toolu_abc)] Arguments: { "action": "act", "request": "click button" }`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("Let me check that for you."); }); it("preserves trailing text after downgraded tool call blocks", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: `Intro text. [Tool Call: read (ID: toolu_1)] Arguments: { "path": "/tmp/file.txt" } Back to the user.`, }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("Intro text.\nBack to the user."); }); it("handles multiple text blocks with tool calls and results", () => { const msg = makeAssistantMessage({ role: "assistant", content: [ { type: "text", text: "Here's what I found:", }, { type: "text", text: `[Tool Call: read (ID: toolu_1)] Arguments: { "path": "/test.txt" }`, }, { type: "text", text: `[Tool Result for ID toolu_1] File contents here`, }, { type: "text", text: "Done checking.", }, ], timestamp: Date.now(), }); const result = extractAssistantText(msg); expect(result).toBe("Here's what I found:\nDone checking."); }); it("strips reasoning/thinking tag variants", () => { const cases = [ { name: "think tag", text: "El usuario quiere retomar una tarea...Aquí está tu respuesta.", expected: "Aquí está tu respuesta.", }, { name: "think tag with attributes", text: `HiddenVisible`, expected: "Visible", }, { name: "unclosed think tag", text: "Pensando sobre el problema...", expected: "", }, { name: "thinking tag", text: "Beforeinternal reasoningAfter", expected: "BeforeAfter", }, { name: "antthinking tag", text: "Some reasoningThe actual answer.", expected: "The actual answer.", }, { name: "final wrapper", text: "\nAnswer\n", expected: "Answer", }, { name: "thought tag", text: "Internal deliberationFinal response.", expected: "Final response.", }, { name: "multiple think blocks", text: "Startfirst thoughtMiddlesecond thoughtEnd", expected: "StartMiddleEnd", }, ] as const; for (const testCase of cases) { const msg = makeAssistantMessage({ role: "assistant", content: [{ type: "text", text: testCase.text }], timestamp: Date.now(), }); expect(extractAssistantText(msg), testCase.name).toBe(testCase.expected); } }); }); describe("formatReasoningMessage", () => { it("returns empty string for whitespace-only input", () => { expect(formatReasoningMessage(" \n \t ")).toBe(""); }); it("wraps single line in italics", () => { expect(formatReasoningMessage("Single line of reasoning")).toBe( "Reasoning:\n_Single line of reasoning_", ); }); it("wraps each line separately for multiline text (Telegram fix)", () => { expect(formatReasoningMessage("Line one\nLine two\nLine three")).toBe( "Reasoning:\n_Line one_\n_Line two_\n_Line three_", ); }); it("preserves empty lines between reasoning text", () => { expect(formatReasoningMessage("First block\n\nSecond block")).toBe( "Reasoning:\n_First block_\n\n_Second block_", ); }); it("handles mixed empty and non-empty lines", () => { expect(formatReasoningMessage("A\n\nB\nC")).toBe("Reasoning:\n_A_\n\n_B_\n_C_"); }); it("trims leading/trailing whitespace", () => { expect(formatReasoningMessage(" \n Reasoning here \n ")).toBe( "Reasoning:\n_Reasoning here_", ); }); }); describe("stripDowngradedToolCallText", () => { it("strips downgraded marker blocks while preserving surrounding user-facing text", () => { const cases = [ { name: "historical context only", text: `[Historical context: a different model called tool "exec" with arguments {"command":"git status"}]`, expected: "", }, { name: "text before historical context", text: `Here is the answer.\n[Historical context: a different model called tool "read"]`, expected: "Here is the answer.", }, { name: "text around historical context", text: `Before.\n[Historical context: tool call info]\nAfter.`, expected: "Before.\nAfter.", }, { name: "multiple historical context blocks", text: `[Historical context: first tool call]\n[Historical context: second tool call]`, expected: "", }, { name: "mixed tool call and historical context", text: `Intro.\n[Tool Call: exec (ID: toolu_1)]\nArguments: { "command": "ls" }\n[Historical context: a different model called tool "read"]`, expected: "Intro.", }, { name: "no markers", text: "Just a normal response with no markers.", expected: "Just a normal response with no markers.", }, ] as const; for (const testCase of cases) { expect(stripDowngradedToolCallText(testCase.text), testCase.name).toBe(testCase.expected); } }); }); describe("promoteThinkingTagsToBlocks", () => { it("does not crash on malformed null content entries", () => { const msg = makeAssistantMessage({ role: "assistant", content: [null as never, { type: "text", text: "hellook" }], timestamp: Date.now(), }); expect(() => promoteThinkingTagsToBlocks(msg)).not.toThrow(); const types = msg.content.map((b: { type?: string }) => b?.type); expect(types).toContain("thinking"); expect(types).toContain("text"); }); it("does not crash on undefined content entries", () => { const msg = makeAssistantMessage({ role: "assistant", content: [undefined as never, { type: "text", text: "no tags here" }], timestamp: Date.now(), }); expect(() => promoteThinkingTagsToBlocks(msg)).not.toThrow(); }); it("passes through well-formed content unchanged when no thinking tags", () => { const msg = makeAssistantMessage({ role: "assistant", content: [{ type: "text", text: "hello world" }], timestamp: Date.now(), }); promoteThinkingTagsToBlocks(msg); expect(msg.content).toEqual([{ type: "text", text: "hello world" }]); }); }); describe("empty input handling", () => { it("returns empty string", () => { const helpers = [formatReasoningMessage, stripDowngradedToolCallText]; for (const helper of helpers) { expect(helper("")).toBe(""); } }); });