Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import { URI } from '../../../../base/common/uri.js';
8
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
9
import { getInvocationMessage, getPastTenseMessage, getPermissionDisplay, getShellLanguage, getToolInputString, getToolKind, isHiddenTool, synthesizeSkillToolCall, type ITypedPermissionRequest } from '../../node/copilot/copilotToolDisplay.js';
10
11
suite('getPermissionDisplay — cd-prefix stripping', () => {
12
13
ensureNoDisposablesAreLeakedInTestSuite();
14
15
const wd = URI.file('/repo/project');
16
17
test('strips redundant cd from shell permission request fullCommandText', () => {
18
const request: ITypedPermissionRequest = {
19
kind: 'shell',
20
fullCommandText: 'cd /repo/project && npm test',
21
} as ITypedPermissionRequest;
22
const display = getPermissionDisplay(request, wd);
23
assert.strictEqual(display.toolInput, 'npm test');
24
assert.strictEqual(display.permissionKind, 'shell');
25
});
26
27
test('leaves shell command alone when cd target differs from working directory', () => {
28
const request: ITypedPermissionRequest = {
29
kind: 'shell',
30
fullCommandText: 'cd /tmp && ls',
31
} as ITypedPermissionRequest;
32
const display = getPermissionDisplay(request, wd);
33
assert.strictEqual(display.toolInput, 'cd /tmp && ls');
34
});
35
36
test('leaves shell command alone when no working directory provided', () => {
37
const request: ITypedPermissionRequest = {
38
kind: 'shell',
39
fullCommandText: 'cd /repo/project && npm test',
40
} as ITypedPermissionRequest;
41
const display = getPermissionDisplay(request, undefined);
42
assert.strictEqual(display.toolInput, 'cd /repo/project && npm test');
43
});
44
45
test('strips redundant cd from custom-tool shell permission request', () => {
46
const request: ITypedPermissionRequest = {
47
kind: 'custom-tool',
48
toolName: 'bash',
49
args: { command: 'cd /repo/project && echo hi' },
50
} as ITypedPermissionRequest;
51
const display = getPermissionDisplay(request, wd);
52
assert.strictEqual(display.toolInput, 'echo hi');
53
assert.strictEqual(display.permissionKind, 'shell');
54
});
55
56
test('does not affect non-shell custom-tool requests', () => {
57
const request: ITypedPermissionRequest = {
58
kind: 'custom-tool',
59
toolName: 'some_other_tool',
60
args: { command: 'cd /repo/project && echo hi' },
61
} as ITypedPermissionRequest;
62
const display = getPermissionDisplay(request, wd);
63
// Falls through to the generic branch — toolInput is the JSON-stringified args.
64
assert.ok(display.toolInput?.includes('cd /repo/project'), `expected unrewritten args, got: ${display.toolInput}`);
65
assert.strictEqual(display.permissionKind, 'custom-tool');
66
});
67
68
test('handles powershell custom-tool with semicolon separator', () => {
69
const request: ITypedPermissionRequest = {
70
kind: 'custom-tool',
71
toolName: 'powershell',
72
args: { command: 'cd /repo/project; dir' },
73
} as ITypedPermissionRequest;
74
const display = getPermissionDisplay(request, wd);
75
assert.strictEqual(display.toolInput, 'dir');
76
});
77
});
78
79
suite('view tool — view_range display', () => {
80
81
ensureNoDisposablesAreLeakedInTestSuite();
82
83
function invocation(parameters: Record<string, unknown> | undefined): string {
84
const result = getInvocationMessage('view', 'View File', parameters);
85
return typeof result === 'string' ? result : result.markdown;
86
}
87
88
function pastTense(parameters: Record<string, unknown> | undefined): string {
89
const result = getPastTenseMessage('view', 'View File', parameters, true);
90
return typeof result === 'string' ? result : result.markdown;
91
}
92
93
test('renders path-only when view_range is absent', () => {
94
assert.ok(invocation({ path: '/repo/file.ts' }).startsWith('Reading ['));
95
assert.ok(pastTense({ path: '/repo/file.ts' }).startsWith('Read ['));
96
});
97
98
test('renders "lines X to Y" for a valid two-element range', () => {
99
assert.ok(invocation({ path: '/repo/file.ts', view_range: [10, 20] }).endsWith(', lines 10 to 20'));
100
assert.ok(pastTense({ path: '/repo/file.ts', view_range: [10, 20] }).endsWith(', lines 10 to 20'));
101
});
102
103
test('renders "line X" when start === end', () => {
104
assert.ok(invocation({ path: '/repo/file.ts', view_range: [10, 10] }).endsWith(', line 10'));
105
assert.ok(pastTense({ path: '/repo/file.ts', view_range: [10, 10] }).endsWith(', line 10'));
106
});
107
108
test('renders "line X to the end" for the -1 EOF sentinel', () => {
109
assert.ok(invocation({ path: '/repo/file.ts', view_range: [10, -1] }).endsWith(', line 10 to the end'));
110
assert.ok(pastTense({ path: '/repo/file.ts', view_range: [10, -1] }).endsWith(', line 10 to the end'));
111
});
112
113
test('falls back to path-only for invalid ranges', () => {
114
// end < start (and not -1)
115
assert.ok(!invocation({ path: '/repo/file.ts', view_range: [20, 10] }).includes(','));
116
// negative start
117
assert.ok(!invocation({ path: '/repo/file.ts', view_range: [-5, 10] }).includes(','));
118
// non-integer
119
assert.ok(!invocation({ path: '/repo/file.ts', view_range: [1.5, 10] }).includes(','));
120
// wrong arity
121
assert.ok(!invocation({ path: '/repo/file.ts', view_range: [10] }).includes(','));
122
assert.ok(!invocation({ path: '/repo/file.ts', view_range: [10, 20, 30] }).includes(','));
123
// non-array
124
assert.ok(!invocation({ path: '/repo/file.ts', view_range: 'whatever' }).includes(','));
125
});
126
});
127
128
// ---- write_/read_ shell tool display ---------------------------------------
129
//
130
// Coverage for the secondary shell helpers (write_bash, read_bash, and their
131
// powershell siblings). These never appear in a permission dialog (they're
132
// registered with `skipPermission: true` — see copilotShellTools.ts), but they
133
// still flow through the tool-execution display pipeline.
134
135
suite('copilotToolDisplay — write_/read_ shell tools', () => {
136
137
ensureNoDisposablesAreLeakedInTestSuite();
138
139
suite('getToolKind', () => {
140
141
test('returns terminal for bash', () => {
142
assert.strictEqual(getToolKind('bash'), 'terminal');
143
});
144
145
test('returns terminal for powershell', () => {
146
assert.strictEqual(getToolKind('powershell'), 'terminal');
147
});
148
149
test('returns undefined for write_bash (sending input to a running program, not launching a terminal)', () => {
150
assert.strictEqual(getToolKind('write_bash'), undefined);
151
});
152
153
test('returns undefined for write_powershell', () => {
154
assert.strictEqual(getToolKind('write_powershell'), undefined);
155
});
156
157
test('returns undefined for read_bash (reading output, not launching a terminal)', () => {
158
assert.strictEqual(getToolKind('read_bash'), undefined);
159
});
160
161
test('returns undefined for read_powershell', () => {
162
assert.strictEqual(getToolKind('read_powershell'), undefined);
163
});
164
165
test('returns subagent for task', () => {
166
assert.strictEqual(getToolKind('task'), 'subagent');
167
});
168
169
test('returns undefined for view', () => {
170
assert.strictEqual(getToolKind('view'), undefined);
171
});
172
});
173
174
suite('getShellLanguage', () => {
175
176
test('bash returns shellscript', () => {
177
assert.strictEqual(getShellLanguage('bash'), 'shellscript');
178
});
179
180
test('powershell returns powershell', () => {
181
assert.strictEqual(getShellLanguage('powershell'), 'powershell');
182
});
183
184
test('write_bash returns shellscript', () => {
185
assert.strictEqual(getShellLanguage('write_bash'), 'shellscript');
186
});
187
188
test('write_powershell returns powershell', () => {
189
assert.strictEqual(getShellLanguage('write_powershell'), 'powershell');
190
});
191
192
test('read_bash returns shellscript', () => {
193
assert.strictEqual(getShellLanguage('read_bash'), 'shellscript');
194
});
195
196
test('read_powershell returns powershell', () => {
197
assert.strictEqual(getShellLanguage('read_powershell'), 'powershell');
198
});
199
});
200
201
suite('getInvocationMessage', () => {
202
203
function getText(msg: ReturnType<typeof getInvocationMessage>): string {
204
return typeof msg === 'string' ? msg : msg.markdown;
205
}
206
207
test('write_bash with command includes the command text', () => {
208
const msg = getInvocationMessage('write_bash', 'Write Shell Input', { command: 'echo hello' });
209
assert.ok(getText(msg).includes('echo hello'), `expected 'echo hello' in: ${getText(msg)}`);
210
});
211
212
test('write_bash without command returns a non-empty fallback message', () => {
213
const msg = getInvocationMessage('write_bash', 'Write Shell Input', undefined);
214
assert.ok(getText(msg).length > 0);
215
assert.ok(!getText(msg).includes('undefined'));
216
});
217
218
test('write_powershell with command includes the command text', () => {
219
const msg = getInvocationMessage('write_powershell', 'Write Shell Input', { command: 'Get-Date' });
220
assert.ok(getText(msg).includes('Get-Date'), `expected 'Get-Date' in: ${getText(msg)}`);
221
});
222
223
test('read_bash returns a non-empty message', () => {
224
const msg = getInvocationMessage('read_bash', 'Read Shell Output', undefined);
225
assert.ok(getText(msg).length > 0);
226
});
227
228
test('read_powershell returns a non-empty message', () => {
229
const msg = getInvocationMessage('read_powershell', 'Read Shell Output', undefined);
230
assert.ok(getText(msg).length > 0);
231
});
232
233
test('write_bash message differs from bash message (distinct wording)', () => {
234
const writeBashMsg = getText(getInvocationMessage('write_bash', 'Write Shell Input', { command: 'echo hi' }));
235
const bashMsg = getText(getInvocationMessage('bash', 'Bash', { command: 'echo hi' }));
236
// Both include the command, but the surrounding text should differ
237
assert.notStrictEqual(writeBashMsg, bashMsg);
238
});
239
});
240
241
suite('getPastTenseMessage', () => {
242
243
function getText(msg: ReturnType<typeof getPastTenseMessage>): string {
244
return typeof msg === 'string' ? msg : msg.markdown;
245
}
246
247
test('write_bash with command includes the command text', () => {
248
const msg = getPastTenseMessage('write_bash', 'Write Shell Input', { command: 'echo hello' }, true);
249
assert.ok(getText(msg).includes('echo hello'), `expected 'echo hello' in: ${getText(msg)}`);
250
});
251
252
test('write_bash without command returns a non-empty fallback message', () => {
253
const msg = getPastTenseMessage('write_bash', 'Write Shell Input', undefined, true);
254
assert.ok(getText(msg).length > 0);
255
});
256
257
test('write_powershell with command includes the command text', () => {
258
const msg = getPastTenseMessage('write_powershell', 'Write Shell Input', { command: 'Get-Date' }, true);
259
assert.ok(getText(msg).includes('Get-Date'), `expected 'Get-Date' in: ${getText(msg)}`);
260
});
261
262
test('read_bash success returns a non-empty message', () => {
263
const msg = getPastTenseMessage('read_bash', 'Read Shell Output', undefined, true);
264
assert.ok(getText(msg).length > 0);
265
});
266
267
test('write_bash failure returns a non-empty error message', () => {
268
const msg = getPastTenseMessage('write_bash', 'Write Shell Input', { command: 'echo hello' }, false);
269
assert.ok(getText(msg).length > 0);
270
});
271
});
272
273
suite('getToolInputString', () => {
274
275
test('write_bash extracts command field', () => {
276
assert.strictEqual(getToolInputString('write_bash', { command: 'echo hello' }, undefined), 'echo hello');
277
});
278
279
test('write_powershell extracts command field', () => {
280
assert.strictEqual(getToolInputString('write_powershell', { command: 'Get-Date' }, undefined), 'Get-Date');
281
});
282
283
test('write_bash falls back to rawArguments when no command field', () => {
284
assert.strictEqual(getToolInputString('write_bash', {}, '{"command":"echo hello"}'), '{"command":"echo hello"}');
285
});
286
287
test('write_bash returns undefined when both parameters and rawArguments are absent', () => {
288
assert.strictEqual(getToolInputString('write_bash', undefined, undefined), undefined);
289
});
290
291
test('read_bash with no parameters returns undefined', () => {
292
assert.strictEqual(getToolInputString('read_bash', undefined, undefined), undefined);
293
});
294
});
295
});
296
297
suite('skill events', () => {
298
299
ensureNoDisposablesAreLeakedInTestSuite();
300
301
test('hides the raw `skill` tool call and synthesizes a tool-start/complete pair from `skill.invoked`', () => {
302
const withPath = synthesizeSkillToolCall(
303
{ name: 'plan', path: '/abs/repo/skills/plan/SKILL.md' },
304
'evt-123',
305
);
306
const noPath = synthesizeSkillToolCall(
307
{ name: 'plan' },
308
undefined,
309
);
310
311
assert.deepStrictEqual({
312
skillIsHidden: isHiddenTool('skill'),
313
withPathToolCallId: withPath.toolCallId,
314
withPathToolName: withPath.toolName,
315
withPathDisplayName: withPath.displayName,
316
withPathInvocation: withPath.invocationMessage,
317
withPathPastTense: withPath.pastTenseMessage,
318
noPathToolCallId: noPath.toolCallId,
319
noPathInvocation: noPath.invocationMessage,
320
noPathPastTense: noPath.pastTenseMessage,
321
}, {
322
skillIsHidden: true,
323
withPathToolCallId: 'synth-skill-evt-123',
324
withPathToolName: 'skill',
325
withPathDisplayName: 'Read Skill',
326
withPathInvocation: { markdown: 'Reading skill [plan](file:///abs/repo/skills/plan/SKILL.md)' },
327
withPathPastTense: { markdown: 'Read skill [plan](file:///abs/repo/skills/plan/SKILL.md)' },
328
noPathToolCallId: 'synth-skill-2108d652',
329
noPathInvocation: 'Reading skill plan',
330
noPathPastTense: 'Read skill plan',
331
});
332
});
333
});
334
335