/** * Regression coverage for compact tool display formatting. * Ensures tool names, actions, and details stay readable and redacted. */ import { describe, expect, it } from "vitest"; import { resolveToolSearchCodeDisplayTarget } from "./tool-display-common.js"; import { resolveExecDetail } from "./tool-display-exec.js"; import { formatToolDetail, formatToolSummary, resolveToolDisplay } from "./tool-display.js"; describe("tool display details", () => { it("summarizes tool-search code targets from described tool ids", () => { expect( resolveToolSearchCodeDisplayTarget({ code: "const tool = await openclaw.tools.describe('openclaw:core:exec'); return await openclaw.tools.call(tool.id, { command: 'echo hi' });", }), ).toEqual({ toolName: "openclaw:core:exec", displayToolName: "exec", displayArgs: { command: "echo hi" }, detail: "echo hi", bridgeVerb: "call", }); }); it("normalizes direct tool-search catalog ids to native display names and args", () => { expect( resolveToolSearchCodeDisplayTarget({ code: 'return await openclaw.tools.call("openclaw:core:exec", { command: "echo hi" });', }), ).toEqual({ toolName: "openclaw:core:exec", displayToolName: "exec", displayArgs: { command: "echo hi" }, detail: "echo hi", bridgeVerb: "call", }); }); it("preserves JS numeric literals in tool-search call args", () => { expect( resolveToolSearchCodeDisplayTarget({ code: 'return await openclaw.tools.call("web_search", { query: "OpenClaw", count: 1e3, limit: +3, threshold: .5 });', })?.displayArgs, ).toEqual({ query: "OpenClaw", count: 1000, limit: 3, threshold: 0.5, }); }); it("skips zero/false values for optional detail fields", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "sessions_spawn", args: { task: "double-message-bug-gpt", label: 0, runTimeoutSeconds: 0, }, }), ); expect(detail).toBe("double-message-bug-gpt"); }); it("includes only truthy boolean details", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "message", args: { action: "react", provider: "discord", to: "chan-1", remove: false, }, }), ); expect(detail).toContain("provider discord"); expect(detail).toContain("to chan-1"); expect(detail).not.toContain("remove"); }); it("keeps positive numbers and true booleans", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "sessions_history", args: { sessionKey: "agent:main:main", limit: 20, includeTools: true, }, }), ); expect(detail).toContain("session agent:main:main"); expect(detail).toContain("limit 20"); expect(detail).toContain("tools true"); }); it("formats read/write/edit with intent-first file detail", () => { const readDetail = formatToolDetail( resolveToolDisplay({ name: "read", args: { file_path: "/tmp/a.txt", offset: 2, limit: 2 }, }), ); const writeDetail = formatToolDetail( resolveToolDisplay({ name: "write", args: { file_path: "/tmp/a.txt", content: "abc" }, }), ); const editDetail = formatToolDetail( resolveToolDisplay({ name: "edit", args: { path: "/tmp/a.txt", newText: "abcd" }, }), ); expect(readDetail).toBe("lines 2-3 from /tmp/a.txt"); expect(writeDetail).toBe("to /tmp/a.txt (3 chars)"); expect(editDetail).toBe("in /tmp/a.txt (4 chars)"); }); it("formats web_search query with quotes", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "web_search", args: { query: "OpenClaw docs", count: 3 }, }), ); expect(detail).toBe('for "OpenClaw docs" (top 3)'); }); it("formats web_search provider query shapes", () => { expect( formatToolDetail( resolveToolDisplay({ name: "web_search", args: { q: "Codex OAuth API key", max_results: 5 }, }), ), ).toBe('for "Codex OAuth API key" (top 5)'); expect( formatToolDetail( resolveToolDisplay({ name: "web_search", args: { search_query: [ { q: "latest Kimi model" }, { q: "latest Gemini model" }, { q: "latest Claude model" }, { q: "latest OpenAI model" }, ], }, }), ), ).toBe('for "latest Kimi model", "latest Gemini model", "latest Claude model"…'); }); it("formats Parallel's native objective + search_queries shape", () => { expect( formatToolDetail( resolveToolDisplay({ name: "web_search", args: { objective: "Find the OpenClaw repository on GitHub", search_queries: ["openclaw github", "openclaw repository"], count: 5, }, }), ), ).toBe( 'for "Find the OpenClaw repository on GitHub", "openclaw github", "openclaw repository" (top 5)', ); }); it("summarizes exec commands with context", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "set -euo pipefail\ngit -C /Users/adityasingh/.openclaw/workspace status --short | head -n 3", workdir: "/Users/adityasingh/.openclaw/workspace", }, }), ); expect(detail).toContain("check git status -> show first 3 lines"); expect(detail).toContain("(agent)"); }); it("summarizes bash commands with the same command explainer", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "bash", args: { command: "sed -n '1,80p' extensions/discord/src/draft-stream.ts" }, detailMode: "explain", }), ); expect(detail).toBe("print lines 1-80 from extensions/discord/src/draft-stream.ts"); }); it("moves cd path to context suffix and appends raw command", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "cd ~/my-project && npm install" }, }), ); expect(detail).toBe("install dependencies (in ~/my-project), `cd ~/my-project && npm install`"); }); it("omits raw command details in explain mode", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "cd ~/my-project && npm install" }, detailMode: "explain", }), ); expect(detail).toBe("install dependencies (in ~/my-project)"); }); it("uses compact workspace markers for common workspace paths", () => { expect( formatToolDetail( resolveToolDisplay({ name: "bash", args: { command: "git fetch", workdir: "/Users/peter/mantis-workspace/openclaw" }, detailMode: "explain", }), ), ).toBe("fetch git changes (agent)"); expect( formatToolDetail( resolveToolDisplay({ name: "bash", args: { command: "git status", workdir: "/Users/peter/Projects/openclaw" }, detailMode: "explain", }), ), ).toBe("check git status (repo)"); expect( formatToolDetail( resolveToolDisplay({ name: "bash", args: { command: "command -v discrawl", workdir: "/root/.openclaw/sandboxes/agent-clawsweeper-sandbox-discor-766423d0", }, detailMode: "explain", }), ), ).toBe("command -v discrawl"); }); it("omits bash and exec names from compact tool summaries", () => { expect( formatToolSummary( resolveToolDisplay({ name: "bash", args: { command: "git fetch", workdir: "/Users/peter/mantis-workspace/openclaw" }, detailMode: "explain", }), ), ).toBe("🛠️ fetch git changes (agent)"); expect( formatToolSummary( resolveToolDisplay({ name: "web_search", args: { query: "OpenClaw docs" }, }), ), ).toBe('🔎 Web Search: for "OpenClaw docs"'); }); it("moves cd path to context suffix with multiple stages and raw command", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "cd ~/my-project && npm install && npm test" }, }), ); expect(detail).toBe( "install dependencies → run tests (in ~/my-project), `cd ~/my-project && npm install && npm test`", ); }); it("moves pushd path to context suffix and appends raw command", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "pushd /tmp && git status" }, }), ); expect(detail).toBe("check git status (in /tmp), `pushd /tmp && git status`"); }); it("clears inferred cwd when popd is stripped from preamble", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "pushd /tmp && popd && npm install" }, }), ); expect(detail).toBe("install dependencies, `pushd /tmp && popd && npm install`"); }); it("moves cd path to context suffix with || separator", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "cd /app || npm install" }, }), ); // || means npm install runs when cd FAILS — cd should NOT be stripped as preamble. // Both stages are summarized; cd is not treated as context prefix. expect(detail).toMatch(/^run cd \/app → install dependencies/); }); it("explicit workdir takes priority over cd path", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "cd /tmp && npm install", workdir: "/app" }, }), ); expect(detail).toBe("install dependencies (in /app), `cd /tmp && npm install`"); }); it("summarizes all stages and appends raw command", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "git fetch && git rebase origin/main" }, }), ); expect(detail).toBe( "fetch git changes → rebase git branch, `git fetch && git rebase origin/main`", ); }); it("falls back to raw command for unknown binaries", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "jj rebase -s abc -d main" }, }), ); expect(detail).toBe("jj rebase -s abc -d main"); }); it("falls back to raw command for unknown binary with cwd", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "mycli deploy --prod", workdir: "/app" }, }), ); expect(detail).toBe("mycli deploy --prod (in /app)"); }); it("keeps multi-stage summary when only some stages are generic", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "cargo build && npm test" }, }), ); // "run cargo build" is generic, but "run tests" is known — keep joined summary expect(detail).toMatch(/^run cargo build → run tests/); }); it("handles standalone cd as raw command", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "cd /tmp" }, }), ); // standalone cd (no following command) — treated as raw since it's generic expect(detail).toBe("cd /tmp"); }); it("handles chained cd commands using last path", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "cd /tmp && cd /app" }, }), ); // both cd's are preamble; last path wins expect(detail).toBe("cd /tmp && cd /app (in /app)"); }); it("respects quotes when splitting preamble separators", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: 'export MSG="foo && bar" && echo test' }, }), ); // The && inside quotes must not be treated as a separator — // summary line should be "print text", not "run export" (which would happen // if the quoted && was mistaken for a real separator). expect(detail).toMatch(/^print text/); }); it("recognizes heredoc/inline script exec details", () => { const pyDetail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "python3 < { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "docker pull pihole/pihole:latest", host: "node", node: "raspberrypi", }, }), ); expect(detail).toContain("node: raspberrypi"); }); it("includes both cwd and node name in exec detail for known commands", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "npm install", workdir: "/app", host: "node", node: "raspberrypi", }, }), ); expect(detail).toContain("(in /app)"); expect(detail).toContain("node: raspberrypi"); }); it("omits node label when node param is absent or empty", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "npm install", host: "gateway" }, }), ); expect(detail).not.toContain("node:"); }); it("omits node label when host is not 'node' even if node is set", () => { for (const host of ["gateway", "sandbox", "auto"]) { const detail = formatToolDetail( resolveToolDisplay({ name: "exec", args: { command: "npm install", host, node: "raspberrypi" }, }), ); expect(detail).not.toContain("node:"); } }); }); describe("compactRawCommand middle truncation", () => { it("preserves start and end of long commands", () => { // Use an unknown binary so resolveExecDetail returns the compact raw form directly. const longCommand = "/opt/custom/bin/my-processor --input /data/warehouse/2024/q1/transactions/raw/batch_001.csv --output /data/warehouse/2024/q1/transactions/processed/batch_001_clean.csv"; const result = resolveExecDetail({ command: longCommand }); // Should contain the start of the command expect(result).toContain("/opt/custom/bin/my-processor"); // Should contain the end (filename) expect(result).toContain("batch_001_clean.csv"); // Should contain the ellipsis for middle truncation expect(result).toContain("…"); // Ellipsis should be in the middle, not at the end expect(result).not.toMatch(/…$/); }); it("does not truncate short commands", () => { // Use an unknown binary so resolveExecDetail returns the compact raw form directly. const result = resolveExecDetail({ command: "/opt/custom/bin/my-tool --version" }); expect(result).toBe("/opt/custom/bin/my-tool --version"); }); it("redacts credential-like tails before middle truncation", () => { // The --token flag and its value sit in the middle of a long command. // Without redaction-before-truncation, middle truncation could cut out // the --token flag context but preserve the raw secret at the tail. const longCommand = "/opt/custom/bin/deploy --region us-east-1 --token sk-proj-ABCDEFGHIJKLMNOP1234567890abcdefghij --output /data/results/deploy-output.json"; const result = resolveExecDetail({ command: longCommand }); // The sk- prefixed token must be redacted (masked) before truncation expect(result).not.toContain("ABCDEFGHIJKLMNOP1234567890abcdefghij"); }); it("uses the canonical tool payload redactor before compacting raw commands", () => { const longCommand = "/opt/custom/bin/deploy --aws-key AKIDABCDEFGHIJKLMNOP1234567890 --output /data/results/deploy-output.json"; const result = resolveExecDetail({ command: longCommand }); expect(result).not.toContain("AKIDABCDEFGHIJKLMNOP1234567890"); expect(result).toContain("AKIDAB…7890"); }); }); describe("coerceDisplayValue middle truncation", () => { it("preserves start and end of long string values", () => { const longPath = "/usr/local/share/very/deeply/nested/directory/structure/" + "a".repeat(150) + "/important-file.txt"; const detail = formatToolDetail( resolveToolDisplay({ name: "sessions_spawn", args: { task: longPath }, }), ); // Should contain the start of the path expect(detail).toContain("/usr/local/share/"); // Should contain the end (filename) expect(detail).toContain("important-file.txt"); // Should contain the ellipsis for middle truncation expect(detail).toContain("…"); }); it("does not truncate short string values", () => { const detail = formatToolDetail( resolveToolDisplay({ name: "sessions_spawn", args: { task: "short-task-name" }, }), ); expect(detail).toBe("short-task-name"); expect(detail).not.toContain("…"); }); it("redacts credential-like values in long generic string details", () => { // A long string whose tail contains a GitHub PAT. Without // redaction-before-truncation, middle truncation could preserve // the raw token at the tail after its prefix context is cut. const longValue = "Deploying service to production cluster with auth ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop and " + "x".repeat(200) + " final-step"; const detail = formatToolDetail( resolveToolDisplay({ name: "sessions_spawn", args: { task: longValue }, }), ); // The ghp_ token must be redacted before truncation expect(detail).not.toContain("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop"); }); it("uses the canonical tool payload redactor before compacting string details", () => { const longValue = "Deploying with AWS key AKIDABCDEFGHIJKLMNOP1234567890 and " + "x".repeat(200) + " final-step"; const detail = formatToolDetail( resolveToolDisplay({ name: "sessions_spawn", args: { task: longValue }, }), ); expect(detail).not.toContain("AKIDABCDEFGHIJKLMNOP1234567890"); expect(detail).toContain("AKIDAB…7890"); }); });