1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 claudeSkillInvocation,
5 classifyToolActivity,
6 collectToolFilePaths,
7 deriveToolActivityPresentation,
8 dynamicToolTitle,
9 dynamicToolSurface,
10 formatReadToolLabel,
11 formatSearchToolLabel,
12 mergeToolActivityData,
13} from "./toolActivity.ts";
14
15describe("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});