| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | claudeSkillInvocation, |
| 5 | classifyToolActivity, |
| 6 | collectToolFilePaths, |
| 7 | deriveToolActivityPresentation, |
| 8 | dynamicToolTitle, |
| 9 | dynamicToolSurface, |
| 10 | formatReadToolLabel, |
| 11 | formatSearchToolLabel, |
| 12 | mergeToolActivityData, |
| 13 | } from "./toolActivity.ts"; |
| 14 | |
| 15 | describe("toolActivity", () => { |
| 16 | it("normalizes command tools to a stable ran-command label", () => { |
| 17 | expect( |
| 18 | deriveToolActivityPresentation({ |
| 19 | itemType: "command_execution", |
| 20 | title: "Terminal", |
| 21 | detail: "Terminal", |
| 22 | data: { |
| 23 | command: "bun run lint", |
| 24 | }, |
| 25 | fallbackSummary: "Terminal", |
| 26 | }), |
| 27 | ).toEqual({ |
| 28 | summary: "Ran command", |
| 29 | detail: "bun run lint", |
| 30 | }); |
| 31 | }); |
| 32 | |
| 33 | it("uses structured file paths for read-file tools when available", () => { |
| 34 | expect( |
| 35 | deriveToolActivityPresentation({ |
| 36 | itemType: "dynamic_tool_call", |
| 37 | title: "Read File", |
| 38 | detail: "Read File", |
| 39 | data: { |
| 40 | kind: "read", |
| 41 | locations: [{ path: "/tmp/app.ts" }], |
| 42 | }, |
| 43 | fallbackSummary: "Read File", |
| 44 | }), |
| 45 | ).toEqual({ |
| 46 | summary: "Read /tmp/app.ts", |
| 47 | }); |
| 48 | }); |
| 49 | |
| 50 | it("drops duplicated generic read-file detail when no path is available", () => { |
| 51 | expect( |
| 52 | deriveToolActivityPresentation({ |
| 53 | itemType: "dynamic_tool_call", |
| 54 | title: "Read File", |
| 55 | detail: "Read File", |
| 56 | data: { |
| 57 | kind: "read", |
| 58 | rawInput: {}, |
| 59 | }, |
| 60 | fallbackSummary: "Read File", |
| 61 | }), |
| 62 | ).toEqual({ |
| 63 | summary: "Read file", |
| 64 | }); |
| 65 | }); |
| 66 | |
| 67 | it("classifies from kind and toolName without sniffing titles", () => { |
| 68 | expect(classifyToolActivity({ data: { kind: "read" } })).toBe("read"); |
| 69 | expect(classifyToolActivity({ data: { toolName: "Grep" } })).toBe("search"); |
| 70 | expect(classifyToolActivity({ data: { toolName: "Read" } })).toBe("read"); |
| 71 | for (const toolName of ["github.read_file", "mongodb.find", "mcp__github__read_file"]) { |
| 72 | expect(classifyToolActivity({ data: { toolName } })).toBe("other"); |
| 73 | } |
| 74 | expect(classifyToolActivity({ title: "Find", data: {} })).toBe("other"); |
| 75 | }); |
| 76 | |
| 77 | it("classifies Claude search tools ahead of their broad file-read request kind", () => { |
| 78 | for (const toolName of ["Glob", "Grep", "LS"]) { |
| 79 | expect(classifyToolActivity({ requestKind: "file-read", data: { toolName } })).toBe("search"); |
| 80 | } |
| 81 | expect(classifyToolActivity({ requestKind: "file-read", data: { toolName: "Read" } })).toBe( |
| 82 | "read", |
| 83 | ); |
| 84 | }); |
| 85 | |
| 86 | it("formats read and search labels from structured input", () => { |
| 87 | expect(formatReadToolLabel("src/env.ts")).toBe("Read src/env.ts"); |
| 88 | expect(formatReadToolLabel("src/env.ts", 2)).toBe("Read src/env.ts +2 more"); |
| 89 | expect(formatReadToolLabel("")).toBe("Read file"); |
| 90 | expect( |
| 91 | formatSearchToolLabel({ |
| 92 | input: { pattern: "TODO", path: "apps/web" }, |
| 93 | }), |
| 94 | ).toBe("Searched TODO in web"); |
| 95 | expect( |
| 96 | formatSearchToolLabel({ |
| 97 | input: { glob: "*.ts", path: "/tmp/t3chat-new" }, |
| 98 | }), |
| 99 | ).toBe("Searched files *.ts in t3chat-new"); |
| 100 | expect( |
| 101 | formatSearchToolLabel({ rawInput: {}, input: { pattern: "TODO", path: "apps/web" } }), |
| 102 | ).toBe("Searched TODO in web"); |
| 103 | expect(formatSearchToolLabel({ input: { globPattern: "*.tsx", path: "apps/web" } })).toBe( |
| 104 | "Searched files *.tsx in web", |
| 105 | ); |
| 106 | expect( |
| 107 | formatSearchToolLabel({ input: { pattern: "TODO", glob: "*.ts", path: "apps/web" } }), |
| 108 | ).toBe("Searched TODO in web"); |
| 109 | }); |
| 110 | |
| 111 | it("keeps bare filenames from explicit path fields", () => { |
| 112 | expect(collectToolFilePaths({ input: { file_path: "README" } })).toEqual(["README"]); |
| 113 | }); |
| 114 | |
| 115 | it("keeps the first non-empty rawInput when a later update is empty", () => { |
| 116 | expect( |
| 117 | mergeToolActivityData({ rawInput: { path: "src/a.ts" } }, { rawInput: {}, kind: "read" }), |
| 118 | ).toEqual({ |
| 119 | rawInput: { path: "src/a.ts" }, |
| 120 | kind: "read", |
| 121 | }); |
| 122 | expect( |
| 123 | mergeToolActivityData({ rawInput: { path: "src/a.ts" } }, { rawInput: { startLine: 4 } }), |
| 124 | ).toEqual({ rawInput: { path: "src/a.ts", startLine: 4 } }); |
| 125 | }); |
| 126 | |
| 127 | it.each(["cua_repl.js", "node_repl.js", "js"])("uses the action title for %s", (toolName) => { |
| 128 | expect(dynamicToolTitle(toolName, { title: " Read the current T3 desktop window " })).toBe( |
| 129 | "Read the current T3 desktop window", |
| 130 | ); |
| 131 | const fallback = toolName === "cua_repl.js" ? "Computer use" : "Run JavaScript"; |
| 132 | expect(dynamicToolTitle(toolName, { title: " " })).toBe(fallback); |
| 133 | expect(dynamicToolTitle(toolName, { title: 123 })).toBe(fallback); |
| 134 | expect(dynamicToolTitle(toolName, {})).toBe(fallback); |
| 135 | expect(dynamicToolTitle(toolName, { code: "await sky.get_app_state(app);" })).toBe( |
| 136 | "Computer use", |
| 137 | ); |
| 138 | }); |
| 139 | |
| 140 | it("keeps a retained action title but replaces raw REPL names", () => { |
| 141 | expect(dynamicToolTitle("js", {}, "Read the window")).toBe("Read the window"); |
| 142 | expect(dynamicToolTitle("js", { title: "Inspect the window" }, "Read the window")).toBe( |
| 143 | "Inspect the window", |
| 144 | ); |
| 145 | expect(dynamicToolTitle("js", { code: "await sky.click(app);" }, "Js")).toBe("Computer use"); |
| 146 | expect(dynamicToolTitle("node_repl.js", {}, "Node_repl.js")).toBe("Run JavaScript"); |
| 147 | }); |
| 148 | |
| 149 | it("recognizes computer-use REPL calls before result metadata exists", () => { |
| 150 | expect(dynamicToolSurface("cua_repl.js", {})).toBe("computer"); |
| 151 | for (const name of ["node_repl.js", "js"]) { |
| 152 | expect(dynamicToolSurface(name, { code: "await sky.get_app_state(app);" })).toBe("computer"); |
| 153 | expect(dynamicToolSurface(name, { code: "const sky = await import('@oai/sky');" })).toBe( |
| 154 | "computer", |
| 155 | ); |
| 156 | expect(dynamicToolSurface(name, { code: "await fs.readFile(path);" })).toBeUndefined(); |
| 157 | } |
| 158 | expect(dynamicToolSurface("github.js", { code: "await sky.click(app);" })).toBeUndefined(); |
| 159 | }); |
| 160 | |
| 161 | it("leaves ordinary tool titles in their arguments", () => { |
| 162 | expect(dynamicToolTitle("github.create_issue", { title: "Issue title" })).toBeUndefined(); |
| 163 | }); |
| 164 | |
| 165 | it("titles Claude skill calls with the skill they load", () => { |
| 166 | expect(dynamicToolTitle("Skill", { skill: "full-send" })).toBe("Skill: full-send"); |
| 167 | expect(claudeSkillInvocation("Skill", { skill: "claude-api", args: " pricing " })).toEqual({ |
| 168 | name: "claude-api", |
| 169 | args: "pricing", |
| 170 | }); |
| 171 | expect(dynamicToolTitle("Skill", { skill: " " })).toBeUndefined(); |
| 172 | expect(dynamicToolTitle("Read", { skill: "full-send" })).toBeUndefined(); |
| 173 | }); |
| 174 | }); |