diff options
| author | Adam Malczewski <[email protected]> | 2026-10-15 09:19:43 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-15 09:19:43 +0900 |
| commit | 633b94326ec4b33fe19d65d02ef78d8a6b14e4e9 (patch) | |
| tree | da640b98bf65fba2cc5c1e96354b990ef7c04a63 /src/features/chat/logic/tool-summary.test.ts | |
| parent | ead9530e661a48050988450b6a70d2240f2ea8b1 (diff) | |
| download | dispatch-web-633b94326ec4b33fe19d65d02ef78d8a6b14e4e9.tar.gz dispatch-web-633b94326ec4b33fe19d65d02ef78d8a6b14e4e9.zip | |
feat(tool-call-cards): English tool names, no '{}' for argument-less calls, thinking-style dots while running
Diffstat (limited to 'src/features/chat/logic/tool-summary.test.ts')
| -rw-r--r-- | src/features/chat/logic/tool-summary.test.ts | 81 |
1 files changed, 69 insertions, 12 deletions
diff --git a/src/features/chat/logic/tool-summary.test.ts b/src/features/chat/logic/tool-summary.test.ts index 876ede2..31c7002 100644 --- a/src/features/chat/logic/tool-summary.test.ts +++ b/src/features/chat/logic/tool-summary.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { ToolEntry } from "../../../core/chunks"; -import { viewToolEntry } from "./tool-summary"; +import { toolLabel, viewToolEntry } from "./tool-summary"; function entry( name: string, @@ -23,6 +23,44 @@ const view = (name: string, input: unknown, content: string | null = "ok", isErr viewToolEntry(entry(name, input, content, isError)); const lines = (n: number) => Array.from({ length: n }, (_, i) => `line ${i + 1}`); +describe("tool labels", () => { + it.each([ + ["run_shell", "Run command"], + ["read_file", "Read file"], + ["edit_file", "Edit file"], + ["write_file", "Write file"], + ["web_search", "Web search"], + ["lsp", "Code intelligence"], + ["todo_write", "Update to-dos"], + ["load_skill", "Load skill"], + ["consult_vision", "Ask vision model"], + ["read_image", "Read image"], + ["youtube_transcript", "YouTube transcript"], + ["summon_agent", "Summon agent"], + ["send_agent_message", "Message agent"], + ["read_agent", "Read agent reply"], + ["open_agent", "Open agent"], + ["cancel_agent_message", "Cancel agent message"], + ["list_agents", "List agents"], + ["list_agent_models", "List agent models"], + ])("built-in %s → %s", (name, label) => { + expect(toolLabel(name)).toBe(label); + expect(view(name, {}).label).toBe(label); + expect(view(name, {}).name).toBe(name); + }); + it.each([ + ["github__create_issue", "Github: Create issue"], + ["mcp__github__create_issue", "Github: Create issue"], + ["playwright__browserTakeScreenshot", "Playwright: Browser take screenshot"], + ["scaffold_echo", "Scaffold echo"], + ["fetchUrl", "Fetch url"], + ["some-kebab-tool", "Some kebab tool"], + ["toString", "To string"], + ])("unknown %s is humanized → %s", (name, label) => { + expect(toolLabel(name)).toBe(label); + }); +}); + describe("tool summaries", () => { it.each([ ["run_shell", { command: "bun run test\n --run" }, "bun run test --run"], @@ -156,20 +194,18 @@ describe("tool summaries", () => { ])("generic fallback recognizes %s", (key) => { expect(view("mcp__foo", { [key]: "target" }).callSummary).toBe("target"); }); - it("generic fallback prefers the first meaningful argument, else compact JSON", () => { + it("generic fallback prefers the first meaningful argument, else readable key: value pairs", () => { expect(view("mcp__foo", { command: " ", path: "a", query: "b" }).callSummary).toBe("a"); - expect(view("mcp__foo", { a: 1, b: "x" }).callSummary).toBe('{"a":1,"b":"x"}'); + expect(view("mcp__foo", { a: 1, b: "x" }).callSummary).toBe("a: 1 · b: x"); + // Only nested values → no summary rather than a JSON dump. + expect(view("mcp__foo", { opts: { deep: true } }).callSummary).toBe(""); }); it.each([ "not JSON", "{bad", - null, - undefined, 42, true, [1, "a"], - {}, - "null", '"hi"', ])("tolerates malformed/non-object input %j", (input) => { expect(() => view("read_file", input)).not.toThrow(); @@ -177,6 +213,20 @@ describe("tool summaries", () => { expect(view("read_file", input).callDetail.kind).toBe("fields"); }); it.each([ + null, + undefined, + {}, + [], + "", + " ", + "null", + "{}", + ])("an argument-less call (%j) shows no summary and a 'no arguments' detail — never `{}`", (input) => { + const card = view("list_agent_models", input); + expect(card.callSummary).toBe(""); + expect(card.callDetail).toEqual({ kind: "none" }); + }); + it.each([ "run_shell", "read_file", "edit_file", @@ -190,10 +240,12 @@ describe("tool summaries", () => { "summon_agent", "read_agent", ])("%s falls back safely for missing arguments", (name) => { - for (const input of [null, undefined, {}, "{bad"]) { + for (const input of [null, undefined, {}]) { expect(() => view(name, input)).not.toThrow(); - expect(view(name, input).callSummary.trim()).not.toBe(""); + expect(view(name, input).callSummary).toBe(""); + expect(view(name, input).callDetail).toEqual({ kind: "none" }); } + expect(view(name, "{bad").callSummary).toBe("{bad"); }); it("parses JSON-string inputs", () => { const card = view("run_shell", '{"command":"bun run test"}'); @@ -201,10 +253,11 @@ describe("tool summaries", () => { expect(card.callDetail).toEqual({ kind: "command", text: "bun run test" }); }); it("handles unexpected built-in argument types and prototype-looking tool names", () => { - expect(view("todo_write", { todos: "wrong" }).callSummary).toBe('{"todos":"wrong"}'); + expect(view("todo_write", { todos: "wrong" }).callSummary).toBe("todos: wrong"); expect(view("run_shell", { command: { nested: true } }).callDetail.kind).toBe("fields"); expect(view("constructor", { path: "a" }).callSummary).toBe("a"); - expect(view("__proto__", {}).callSummary).toBe("{}"); + expect(view("__proto__", {}).callSummary).toBe(""); + expect(view("__proto__", {}).label).toBe("Proto"); expect( view("mcp", JSON.parse('{"__proto__":{"command":"bad"},"path":"good"}')).callSummary, ).toBe("good"); @@ -212,7 +265,11 @@ describe("tool summaries", () => { it("does not throw on unserializable input", () => { const cyclic: Record<string, unknown> = {}; cyclic.self = cyclic; - expect(view("mcp", cyclic).callSummary).toBe("[unserializable input]"); + expect(view("mcp", cyclic).callSummary).toBe(""); + expect(view("mcp", cyclic).callDetail).toEqual({ + kind: "fields", + fields: [{ key: "self", value: "[unserializable input]", multiline: false }], + }); expect(() => view("mcp", { integer: 1n })).not.toThrow(); }); it("caps and whitespace-collapses header strings but retains full detail", () => { |
