|
1 | | -import { describe, expect, test } from "bun:test"; |
| 1 | +import { describe, expect, test } from 'bun:test' |
2 | 2 | import { |
3 | 3 | buildTool, |
4 | 4 | toolMatchesName, |
5 | 5 | findToolByName, |
6 | 6 | getEmptyToolPermissionContext, |
7 | 7 | filterToolProgressMessages, |
8 | | -} from "../Tool"; |
| 8 | +} from '../Tool' |
9 | 9 |
|
10 | 10 | // Minimal tool definition for testing buildTool |
11 | 11 | function makeMinimalToolDef(overrides: Record<string, unknown> = {}) { |
12 | 12 | return { |
13 | | - name: "TestTool", |
14 | | - inputSchema: { type: "object" as const } as any, |
| 13 | + name: 'TestTool', |
| 14 | + inputSchema: { type: 'object' as const } as any, |
15 | 15 | maxResultSizeChars: 10000, |
16 | | - call: async () => ({ data: "ok" }), |
17 | | - description: async () => "A test tool", |
18 | | - prompt: async () => "test prompt", |
19 | | - mapToolResultToToolResultBlockParam: (content: unknown, toolUseID: string) => ({ |
20 | | - type: "tool_result" as const, |
| 16 | + call: async () => ({ data: 'ok' }), |
| 17 | + description: async () => 'A test tool', |
| 18 | + prompt: async () => 'test prompt', |
| 19 | + mapToolResultToToolResultBlockParam: ( |
| 20 | + content: unknown, |
| 21 | + toolUseID: string, |
| 22 | + ) => ({ |
| 23 | + type: 'tool_result' as const, |
21 | 24 | tool_use_id: toolUseID, |
22 | 25 | content: String(content), |
23 | 26 | }), |
24 | 27 | renderToolUseMessage: () => null, |
25 | 28 | ...overrides, |
26 | | - }; |
| 29 | + } |
27 | 30 | } |
28 | 31 |
|
29 | | -describe("buildTool", () => { |
30 | | - test("fills in default isEnabled as true", () => { |
31 | | - const tool = buildTool(makeMinimalToolDef()); |
32 | | - expect(tool.isEnabled()).toBe(true); |
33 | | - }); |
34 | | - |
35 | | - test("fills in default isConcurrencySafe as false", () => { |
36 | | - const tool = buildTool(makeMinimalToolDef()); |
37 | | - expect(tool.isConcurrencySafe({})).toBe(false); |
38 | | - }); |
39 | | - |
40 | | - test("fills in default isReadOnly as false", () => { |
41 | | - const tool = buildTool(makeMinimalToolDef()); |
42 | | - expect(tool.isReadOnly({})).toBe(false); |
43 | | - }); |
44 | | - |
45 | | - test("fills in default isDestructive as false", () => { |
46 | | - const tool = buildTool(makeMinimalToolDef()); |
47 | | - expect(tool.isDestructive!({})).toBe(false); |
48 | | - }); |
49 | | - |
50 | | - test("fills in default checkPermissions as allow", async () => { |
51 | | - const tool = buildTool(makeMinimalToolDef()); |
52 | | - const input = { foo: "bar" }; |
53 | | - const result = await tool.checkPermissions(input, {} as any); |
54 | | - expect(result).toEqual({ behavior: "allow", updatedInput: input }); |
55 | | - }); |
56 | | - |
57 | | - test("fills in default userFacingName from tool name", () => { |
58 | | - const tool = buildTool(makeMinimalToolDef()); |
59 | | - expect(tool.userFacingName(undefined)).toBe("TestTool"); |
60 | | - }); |
61 | | - |
62 | | - test("fills in default toAutoClassifierInput as empty string", () => { |
63 | | - const tool = buildTool(makeMinimalToolDef()); |
64 | | - expect(tool.toAutoClassifierInput({})).toBe(""); |
65 | | - }); |
66 | | - |
67 | | - test("preserves explicitly provided methods", () => { |
| 32 | +describe('buildTool', () => { |
| 33 | + test('fills in default isEnabled as true', () => { |
| 34 | + const tool = buildTool(makeMinimalToolDef()) |
| 35 | + expect(tool.isEnabled()).toBe(true) |
| 36 | + }) |
| 37 | + |
| 38 | + test('fills in default isConcurrencySafe as false', () => { |
| 39 | + const tool = buildTool(makeMinimalToolDef()) |
| 40 | + expect(tool.isConcurrencySafe({})).toBe(false) |
| 41 | + }) |
| 42 | + |
| 43 | + test('fills in default isReadOnly as false', () => { |
| 44 | + const tool = buildTool(makeMinimalToolDef()) |
| 45 | + expect(tool.isReadOnly({})).toBe(false) |
| 46 | + }) |
| 47 | + |
| 48 | + test('fills in default isDestructive as false', () => { |
| 49 | + const tool = buildTool(makeMinimalToolDef()) |
| 50 | + expect(tool.isDestructive!({})).toBe(false) |
| 51 | + }) |
| 52 | + |
| 53 | + test('fills in default checkPermissions as allow', async () => { |
| 54 | + const tool = buildTool(makeMinimalToolDef()) |
| 55 | + const input = { foo: 'bar' } |
| 56 | + const result = await tool.checkPermissions(input, {} as any) |
| 57 | + expect(result).toEqual({ behavior: 'allow', updatedInput: input }) |
| 58 | + }) |
| 59 | + |
| 60 | + test('fills in default userFacingName from tool name', () => { |
| 61 | + const tool = buildTool(makeMinimalToolDef()) |
| 62 | + expect(tool.userFacingName(undefined)).toBe('TestTool') |
| 63 | + }) |
| 64 | + |
| 65 | + test('fills in default toAutoClassifierInput as empty string', () => { |
| 66 | + const tool = buildTool(makeMinimalToolDef()) |
| 67 | + expect(tool.toAutoClassifierInput({})).toBe('') |
| 68 | + }) |
| 69 | + |
| 70 | + test('preserves explicitly provided methods', () => { |
68 | 71 | const tool = buildTool( |
69 | 72 | makeMinimalToolDef({ |
70 | 73 | isEnabled: () => false, |
71 | 74 | isConcurrencySafe: () => true, |
72 | 75 | isReadOnly: () => true, |
73 | | - }) |
74 | | - ); |
75 | | - expect(tool.isEnabled()).toBe(false); |
76 | | - expect(tool.isConcurrencySafe({})).toBe(true); |
77 | | - expect(tool.isReadOnly({})).toBe(true); |
78 | | - }); |
79 | | - |
80 | | - test("preserves all non-defaultable properties", () => { |
81 | | - const tool = buildTool(makeMinimalToolDef()); |
82 | | - expect(tool.name).toBe("TestTool"); |
83 | | - expect(tool.maxResultSizeChars).toBe(10000); |
84 | | - expect(typeof tool.call).toBe("function"); |
85 | | - expect(typeof tool.description).toBe("function"); |
86 | | - expect(typeof tool.prompt).toBe("function"); |
87 | | - }); |
88 | | -}); |
89 | | - |
90 | | -describe("toolMatchesName", () => { |
91 | | - test("returns true for exact name match", () => { |
92 | | - expect(toolMatchesName({ name: "Bash" }, "Bash")).toBe(true); |
93 | | - }); |
94 | | - |
95 | | - test("returns false for non-matching name", () => { |
96 | | - expect(toolMatchesName({ name: "Bash" }, "Read")).toBe(false); |
97 | | - }); |
98 | | - |
99 | | - test("returns true when name matches an alias", () => { |
| 76 | + }), |
| 77 | + ) |
| 78 | + expect(tool.isEnabled()).toBe(false) |
| 79 | + expect(tool.isConcurrencySafe({})).toBe(true) |
| 80 | + expect(tool.isReadOnly({})).toBe(true) |
| 81 | + }) |
| 82 | + |
| 83 | + test('preserves all non-defaultable properties', () => { |
| 84 | + const tool = buildTool(makeMinimalToolDef()) |
| 85 | + expect(tool.name).toBe('TestTool') |
| 86 | + expect(tool.maxResultSizeChars).toBe(10000) |
| 87 | + expect(typeof tool.call).toBe('function') |
| 88 | + expect(typeof tool.description).toBe('function') |
| 89 | + expect(typeof tool.prompt).toBe('function') |
| 90 | + }) |
| 91 | +}) |
| 92 | + |
| 93 | +describe('toolMatchesName', () => { |
| 94 | + test('returns true for exact name match', () => { |
| 95 | + expect(toolMatchesName({ name: 'Bash' }, 'Bash')).toBe(true) |
| 96 | + }) |
| 97 | + |
| 98 | + test('returns false for non-matching name', () => { |
| 99 | + expect(toolMatchesName({ name: 'Bash' }, 'Read')).toBe(false) |
| 100 | + }) |
| 101 | + |
| 102 | + test('returns true when name matches an alias', () => { |
100 | 103 | expect( |
101 | | - toolMatchesName({ name: "Bash", aliases: ["BashTool", "Shell"] }, "BashTool") |
102 | | - ).toBe(true); |
103 | | - }); |
104 | | - |
105 | | - test("returns false when aliases is undefined", () => { |
106 | | - expect(toolMatchesName({ name: "Bash" }, "BashTool")).toBe(false); |
107 | | - }); |
108 | | - |
109 | | - test("returns false when aliases is empty", () => { |
110 | | - expect( |
111 | | - toolMatchesName({ name: "Bash", aliases: [] }, "BashTool") |
112 | | - ).toBe(false); |
113 | | - }); |
114 | | -}); |
115 | | - |
116 | | -describe("findToolByName", () => { |
| 104 | + toolMatchesName( |
| 105 | + { name: 'Bash', aliases: ['BashTool', 'Shell'] }, |
| 106 | + 'BashTool', |
| 107 | + ), |
| 108 | + ).toBe(true) |
| 109 | + }) |
| 110 | + |
| 111 | + test('returns false when aliases is undefined', () => { |
| 112 | + expect(toolMatchesName({ name: 'Bash' }, 'BashTool')).toBe(false) |
| 113 | + }) |
| 114 | + |
| 115 | + test('returns false when aliases is empty', () => { |
| 116 | + expect(toolMatchesName({ name: 'Bash', aliases: [] }, 'BashTool')).toBe( |
| 117 | + false, |
| 118 | + ) |
| 119 | + }) |
| 120 | +}) |
| 121 | + |
| 122 | +describe('findToolByName', () => { |
117 | 123 | const mockTools = [ |
118 | | - buildTool(makeMinimalToolDef({ name: "Bash" })), |
119 | | - buildTool(makeMinimalToolDef({ name: "Read", aliases: ["FileRead"] })), |
120 | | - buildTool(makeMinimalToolDef({ name: "Edit" })), |
121 | | - ]; |
122 | | - |
123 | | - test("finds tool by primary name", () => { |
124 | | - const tool = findToolByName(mockTools, "Bash"); |
125 | | - expect(tool).toBeDefined(); |
126 | | - expect(tool!.name).toBe("Bash"); |
127 | | - }); |
128 | | - |
129 | | - test("finds tool by alias", () => { |
130 | | - const tool = findToolByName(mockTools, "FileRead"); |
131 | | - expect(tool).toBeDefined(); |
132 | | - expect(tool!.name).toBe("Read"); |
133 | | - }); |
134 | | - |
135 | | - test("returns undefined when no match", () => { |
136 | | - expect(findToolByName(mockTools, "NonExistent")).toBeUndefined(); |
137 | | - }); |
138 | | - |
139 | | - test("returns first match when duplicates exist", () => { |
| 124 | + buildTool(makeMinimalToolDef({ name: 'Bash' })), |
| 125 | + buildTool(makeMinimalToolDef({ name: 'Read', aliases: ['FileRead'] })), |
| 126 | + buildTool(makeMinimalToolDef({ name: 'Edit' })), |
| 127 | + ] |
| 128 | + |
| 129 | + test('finds tool by primary name', () => { |
| 130 | + const tool = findToolByName(mockTools, 'Bash') |
| 131 | + expect(tool).toBeDefined() |
| 132 | + expect(tool!.name).toBe('Bash') |
| 133 | + }) |
| 134 | + |
| 135 | + test('finds tool by alias', () => { |
| 136 | + const tool = findToolByName(mockTools, 'FileRead') |
| 137 | + expect(tool).toBeDefined() |
| 138 | + expect(tool!.name).toBe('Read') |
| 139 | + }) |
| 140 | + |
| 141 | + test('returns undefined when no match', () => { |
| 142 | + expect(findToolByName(mockTools, 'NonExistent')).toBeUndefined() |
| 143 | + }) |
| 144 | + |
| 145 | + test('returns first match when duplicates exist', () => { |
140 | 146 | const dupeTools = [ |
141 | | - buildTool(makeMinimalToolDef({ name: "Bash", maxResultSizeChars: 100 })), |
142 | | - buildTool(makeMinimalToolDef({ name: "Bash", maxResultSizeChars: 200 })), |
143 | | - ]; |
144 | | - const tool = findToolByName(dupeTools, "Bash"); |
145 | | - expect(tool!.maxResultSizeChars).toBe(100); |
146 | | - }); |
147 | | -}); |
148 | | - |
149 | | -describe("getEmptyToolPermissionContext", () => { |
150 | | - test("returns default permission mode", () => { |
151 | | - const ctx = getEmptyToolPermissionContext(); |
152 | | - expect(ctx.mode).toBe("default"); |
153 | | - }); |
154 | | - |
155 | | - test("returns empty maps and arrays", () => { |
156 | | - const ctx = getEmptyToolPermissionContext(); |
157 | | - expect(ctx.additionalWorkingDirectories.size).toBe(0); |
158 | | - expect(ctx.alwaysAllowRules).toEqual({}); |
159 | | - expect(ctx.alwaysDenyRules).toEqual({}); |
160 | | - expect(ctx.alwaysAskRules).toEqual({}); |
161 | | - }); |
162 | | - |
163 | | - test("returns isBypassPermissionsModeAvailable as false", () => { |
164 | | - const ctx = getEmptyToolPermissionContext(); |
165 | | - expect(ctx.isBypassPermissionsModeAvailable).toBe(false); |
166 | | - }); |
167 | | -}); |
168 | | - |
169 | | -describe("filterToolProgressMessages", () => { |
170 | | - test("filters out hook_progress messages", () => { |
| 147 | + buildTool(makeMinimalToolDef({ name: 'Bash', maxResultSizeChars: 100 })), |
| 148 | + buildTool(makeMinimalToolDef({ name: 'Bash', maxResultSizeChars: 200 })), |
| 149 | + ] |
| 150 | + const tool = findToolByName(dupeTools, 'Bash') |
| 151 | + expect(tool!.maxResultSizeChars).toBe(100) |
| 152 | + }) |
| 153 | +}) |
| 154 | + |
| 155 | +describe('getEmptyToolPermissionContext', () => { |
| 156 | + test('returns default permission mode', () => { |
| 157 | + const ctx = getEmptyToolPermissionContext() |
| 158 | + expect(ctx.mode).toBe('default') |
| 159 | + }) |
| 160 | + |
| 161 | + test('returns empty maps and arrays', () => { |
| 162 | + const ctx = getEmptyToolPermissionContext() |
| 163 | + expect(ctx.additionalWorkingDirectories.size).toBe(0) |
| 164 | + expect(ctx.alwaysAllowRules).toEqual({}) |
| 165 | + expect(ctx.alwaysDenyRules).toEqual({}) |
| 166 | + expect(ctx.alwaysAskRules).toEqual({}) |
| 167 | + }) |
| 168 | + |
| 169 | + test('returns isBypassPermissionsModeAvailable as false', () => { |
| 170 | + const ctx = getEmptyToolPermissionContext() |
| 171 | + expect(ctx.isBypassPermissionsModeAvailable).toBe(false) |
| 172 | + }) |
| 173 | +}) |
| 174 | + |
| 175 | +describe('filterToolProgressMessages', () => { |
| 176 | + test('filters out hook_progress messages', () => { |
171 | 177 | const messages = [ |
172 | | - { data: { type: "hook_progress", hookName: "pre" } }, |
173 | | - { data: { type: "tool_progress", toolName: "Bash" } }, |
174 | | - ] as any[]; |
175 | | - const result = filterToolProgressMessages(messages); |
176 | | - expect(result).toHaveLength(1); |
177 | | - expect((result[0]!.data as any).type).toBe("tool_progress"); |
178 | | - }); |
179 | | - |
180 | | - test("keeps tool progress messages", () => { |
| 178 | + { data: { type: 'hook_progress', hookName: 'pre' } }, |
| 179 | + { data: { type: 'tool_progress', toolName: 'Bash' } }, |
| 180 | + ] as any[] |
| 181 | + const result = filterToolProgressMessages(messages) |
| 182 | + expect(result).toHaveLength(1) |
| 183 | + expect((result[0]!.data as any).type).toBe('tool_progress') |
| 184 | + }) |
| 185 | + |
| 186 | + test('keeps tool progress messages', () => { |
181 | 187 | const messages = [ |
182 | | - { data: { type: "tool_progress", toolName: "Bash" } }, |
183 | | - { data: { type: "tool_progress", toolName: "Read" } }, |
184 | | - ] as any[]; |
185 | | - const result = filterToolProgressMessages(messages); |
186 | | - expect(result).toHaveLength(2); |
187 | | - }); |
188 | | - |
189 | | - test("returns empty array for empty input", () => { |
190 | | - expect(filterToolProgressMessages([])).toEqual([]); |
191 | | - }); |
192 | | - |
193 | | - test("handles messages without type field", () => { |
| 188 | + { data: { type: 'tool_progress', toolName: 'Bash' } }, |
| 189 | + { data: { type: 'tool_progress', toolName: 'Read' } }, |
| 190 | + ] as any[] |
| 191 | + const result = filterToolProgressMessages(messages) |
| 192 | + expect(result).toHaveLength(2) |
| 193 | + }) |
| 194 | + |
| 195 | + test('returns empty array for empty input', () => { |
| 196 | + expect(filterToolProgressMessages([])).toEqual([]) |
| 197 | + }) |
| 198 | + |
| 199 | + test('handles messages without type field', () => { |
194 | 200 | const messages = [ |
195 | | - { data: { toolName: "Bash" } }, |
196 | | - { data: { type: "hook_progress" } }, |
197 | | - ] as any[]; |
198 | | - const result = filterToolProgressMessages(messages); |
199 | | - expect(result).toHaveLength(1); |
200 | | - }); |
201 | | -}); |
| 201 | + { data: { toolName: 'Bash' } }, |
| 202 | + { data: { type: 'hook_progress' } }, |
| 203 | + ] as any[] |
| 204 | + const result = filterToolProgressMessages(messages) |
| 205 | + expect(result).toHaveLength(1) |
| 206 | + }) |
| 207 | +}) |
0 commit comments