/** * tool-call-card.tsx preview helpers: previewArguments keeps the real arguments (what heads * the approval row), headerSubtitle surfaces the model-written `description` argument for * the command/subagent tools and the shortened file path for the file tools, and * pendingFilePayload decodes the file-tool arguments so a pending approval shows the actual * rewrite. All must tolerate incomplete mid-stream JSON; headerSubtitle additionally holds a * still-streaming field back until its closing quote so the header never jitters (#137). */ import { describe, expect, it } from "vitest"; import { headerSubtitle, pendingFilePayload, previewArguments, shortenPath, } from "../src/features/chat/tool-call-card"; describe("previewArguments", () => { it("renders exec_command as $ ", () => { expect(previewArguments("exec_command", '{"cmd":"ls -la"}')).toBe("$ ls -la"); // Mid-stream (incomplete JSON) still extracts the cmd prefix. expect(previewArguments("exec_command", '{"cmd":"echo h')).toBe("$ echo h"); }); it("renders the file tools by their shortened file_path", () => { expect(previewArguments("read_file", '{"file_path":"src/app.py","offset":3}')).toBe( "src/app.py", ); expect( previewArguments("edit_file", '{"file_path":"a.txt","old_string":"x","new_string":"y"}'), ).toBe("a.txt"); expect(previewArguments("write_file", '{"file_path":"packages/core/src/state/out.ts"}')).toBe( "…/state/out.ts", ); }); it("keeps the real command even when a description argument is present (approval fidelity)", () => { expect( previewArguments("exec_command", '{"cmd":"rm -rf build","description":"Clean caches"}'), ).toBe("$ rm -rf build"); }); it("falls back to the single-line raw arguments for other tools", () => { expect(previewArguments("search", '{"q": "a\n b"}')).toBe('{"q": "a b"}'); }); }); describe("shortenPath", () => { it("keeps at most one parent directory plus the filename", () => { expect(shortenPath("file.ts")).toBe("file.ts"); expect(shortenPath("src/file.ts")).toBe("src/file.ts"); expect(shortenPath("/etc/hosts")).toBe("/etc/hosts"); expect(shortenPath("packages/core/src/state/default-config.ts")).toBe( "…/state/default-config.ts", ); }); }); describe("headerSubtitle", () => { it("shows the description for the command/subagent tools when present", () => { expect( headerSubtitle("exec_command", '{"cmd":"ls","description":"List workspace files"}'), ).toBe("List workspace files"); expect( headerSubtitle("run_subagent", '{"prompt":"p","description":"Delegating research"}'), ).toBe("Delegating research"); expect( headerSubtitle("input_command", '{"process_id":"proc-1","description":"Poll the build"}'), ).toBe("Poll the build"); expect( headerSubtitle("input_subagent", '{"subagent_id":"s-1","description":"Follow up"}'), ).toBe("Follow up"); }); it("is null when the description is absent or empty (call_description off = the model never sends it)", () => { expect(headerSubtitle("exec_command", '{"cmd":"ls"}')).toBeNull(); expect(headerSubtitle("exec_command", '{"cmd":"ls","description":""}')).toBeNull(); }); it("shows the shortened file path for the file tools and nothing for others", () => { expect(headerSubtitle("read_file", '{"file_path":"src/app.py"}')).toBe("src/app.py"); expect(headerSubtitle("edit_file", '{"file_path":"packages/web/src/a.tsx"}')).toBe( "…/src/a.tsx", ); expect(headerSubtitle("write_file", '{"file_path":"out.md"}')).toBe("out.md"); expect(headerSubtitle("search", '{"q":"x"}')).toBeNull(); }); it("folds a multi-line description to one line", () => { expect(headerSubtitle("exec_command", '{"cmd":"ls","description":"one\\ntwo"}')).toBe( "one two", ); }); it("holds a still-streaming description back until its closing quote (#137)", () => { expect(headerSubtitle("exec_command", '{"description":"Read the con', false)).toBeNull(); // Closing quote arrived: renders even though later arguments are still streaming. expect( headerSubtitle("exec_command", '{"description":"Read the config","cmd":"cat co', false), ).toBe("Read the config"); }); it("holds a still-streaming file path back (shortenPath would rewrite non-monotonically)", () => { expect(headerSubtitle("read_file", '{"file_path":"/home/us', false)).toBeNull(); expect(headerSubtitle("write_file", '{"file_path":"/a/b/c.txt","content":"xx', false)).toBe( "…/b/c.txt", ); }); it("renders whatever is there once the arguments settled, even unterminated (aborted call)", () => { expect(headerSubtitle("exec_command", '{"description":"half', true)).toBe("half"); expect(headerSubtitle("read_file", '{"file_path":"/a/b/c.txt', true)).toBe("…/b/c.txt"); }); it("prefers a complete description over the file path when a file tool carries one (user-enabled schema)", () => { expect( headerSubtitle("read_file", '{"description":"Check the config","file_path":"/a/b/c.ts"}'), ).toBe("Check the config"); // Description still streaming: nothing renders yet — no path-then-description swap. expect(headerSubtitle("read_file", '{"description":"Check the co', false)).toBeNull(); // Empty description falls back to the path. expect(headerSubtitle("read_file", '{"description":"","file_path":"/a/b/c.ts"}')).toBe( "…/b/c.ts", ); }); }); describe("pendingFilePayload", () => { it("decodes the edit_file rewrite so the approval block shows it", () => { const payload = pendingFilePayload( "edit_file", JSON.stringify({ file_path: "src/app.py", old_string: "a\nb", new_string: "a\nc" }), ); expect(payload).toBe("file_path: src/app.py\nold_string:\na\nb\nnew_string:\na\nc"); }); it("decodes write_file content and read_file window arguments", () => { expect(pendingFilePayload("write_file", '{"file_path":"out.md","content":"hello"}')).toBe( "file_path: out.md\ncontent: hello", ); expect(pendingFilePayload("read_file", '{"file_path":"a.txt","offset":3,"limit":5}')).toBe( "file_path: a.txt\noffset: 3\nlimit: 5", ); }); it("returns null for non-file tools and incomplete JSON", () => { expect(pendingFilePayload("exec_command", '{"cmd":"ls"}')).toBeNull(); expect(pendingFilePayload("edit_file", '{"file_path":"a.txt","old_str')).toBeNull(); }); });