Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/test/chatVariablesHelpers.spec.ts
13406 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 { describe, expect, test } from 'vitest';
7
import { URI } from '../../../../../util/vs/base/common/uri';
8
import { ChatVariablesCollection, getPromptFileSlashCommandId, parseSlashCommand, PromptFileIdPrefix, type PromptVariable } from '../../../../prompt/common/chatVariablesCollection';
9
import { buildSlashCommandUserMessage } from '../chatVariables';
10
11
function makePromptVariable(name: string, value: PromptVariable['value']): PromptVariable {
12
return {
13
reference: { id: name, name, value },
14
originalName: name,
15
uniqueName: name,
16
value,
17
isMarkedReadonly: undefined,
18
};
19
}
20
21
describe('getPromptFileSlashCommandId', () => {
22
test('prompt file uses filename without .prompt.md extension', () => {
23
expect(getPromptFileSlashCommandId(
24
makePromptVariable('prompt:yell-foo.prompt.md', URI.file('/workspace/.github/prompts/yell-foo.prompt.md'))
25
)).toEqual({ name: 'prompt:yell-foo.prompt.md', id: 'yell-foo' });
26
});
27
28
test('skill file uses parent folder name', () => {
29
expect(getPromptFileSlashCommandId(
30
makePromptVariable('code-review', URI.file('/workspace/.github/skills/code-review/SKILL.md'))
31
)).toEqual({ name: 'code-review', id: 'code-review' });
32
});
33
34
test('skill file is case-insensitive for SKILL.md', () => {
35
expect(getPromptFileSlashCommandId(
36
makePromptVariable('my-skill', URI.file('/workspace/.github/skills/my-skill/skill.md'))
37
)).toEqual({ name: 'my-skill', id: 'my-skill' });
38
});
39
40
test('non-prompt, non-skill file falls back to reference name', () => {
41
expect(getPromptFileSlashCommandId(
42
makePromptVariable('some-instructions.instructions.md', URI.file('/workspace/.github/instructions/some-instructions.instructions.md'))
43
)).toEqual({ name: 'some-instructions.instructions.md', id: 'some-instructions.instructions.md' });
44
});
45
46
test('non-URI value falls back to reference name', () => {
47
expect(getPromptFileSlashCommandId(
48
makePromptVariable('inline-ref', 'some string value')
49
)).toEqual({ name: 'inline-ref', id: 'inline-ref' });
50
});
51
52
test('prompt file with nested path', () => {
53
expect(getPromptFileSlashCommandId(
54
makePromptVariable('prompt:deeply-nested.prompt.md', URI.file('/a/b/c/d/deeply-nested.prompt.md'))
55
)).toEqual({ name: 'prompt:deeply-nested.prompt.md', id: 'deeply-nested' });
56
});
57
});
58
59
describe('buildSlashCommandUserMessage', () => {
60
function makeCollection(entries: { name: string; uri: ReturnType<typeof URI.file> }[]): ChatVariablesCollection {
61
return new ChatVariablesCollection(entries.map(e => ({
62
id: `${PromptFileIdPrefix}:${e.name}`,
63
name: e.name,
64
value: e.uri,
65
})));
66
}
67
68
const chatVariables = makeCollection([
69
{ name: 'prompt:code-review.prompt.md', uri: URI.file('/workspace/.github/prompts/code-review.prompt.md') },
70
{ name: 'my-skill', uri: URI.file('/workspace/.github/skills/my-skill/SKILL.md') },
71
]);
72
73
test('returns follow instruction for matching slash command without args', () => {
74
expect(buildSlashCommandUserMessage('/code-review', chatVariables))
75
.toBe('Follow instructions in #prompt:code-review.prompt.md');
76
});
77
78
test('returns follow instruction with arguments when provided', () => {
79
expect(buildSlashCommandUserMessage('/code-review some-file.ts', chatVariables))
80
.toBe('Follow instructions in #prompt:code-review.prompt.md with these arguments: some-file.ts');
81
});
82
83
test('passes multi-word arguments', () => {
84
expect(buildSlashCommandUserMessage('/code-review file1.ts file2.ts --strict', chatVariables))
85
.toBe('Follow instructions in #prompt:code-review.prompt.md with these arguments: file1.ts file2.ts --strict');
86
});
87
88
test('matches skill slash commands', () => {
89
expect(buildSlashCommandUserMessage('/my-skill do something', chatVariables))
90
.toBe('Follow instructions in #my-skill with these arguments: do something');
91
});
92
93
test('returns original query when no slash command', () => {
94
expect(buildSlashCommandUserMessage('just a normal question', chatVariables))
95
.toBe('just a normal question');
96
});
97
98
test('returns original query when slash command does not match any prompt file', () => {
99
expect(buildSlashCommandUserMessage('/unknown-command arg1', chatVariables))
100
.toBe('/unknown-command arg1');
101
});
102
103
test('handles leading whitespace in query', () => {
104
expect(buildSlashCommandUserMessage(' /code-review', chatVariables))
105
.toBe('Follow instructions in #prompt:code-review.prompt.md');
106
});
107
108
test('trims trailing whitespace from arguments', () => {
109
expect(buildSlashCommandUserMessage('/code-review some-file.ts ', chatVariables))
110
.toBe('Follow instructions in #prompt:code-review.prompt.md with these arguments: some-file.ts');
111
});
112
113
test('handles empty prompt file list', () => {
114
expect(buildSlashCommandUserMessage('/code-review', new ChatVariablesCollection([])))
115
.toBe('/code-review');
116
});
117
118
test('handles multiline arguments', () => {
119
expect(buildSlashCommandUserMessage('/code-review line1\nline2', chatVariables))
120
.toBe('Follow instructions in #prompt:code-review.prompt.md with these arguments: line1\nline2');
121
});
122
});
123
124
describe('parseSlashCommand', () => {
125
function makeCollection(entries: { name: string; uri: ReturnType<typeof URI.file> }[]): ChatVariablesCollection {
126
return new ChatVariablesCollection(entries.map(e => ({
127
id: `${PromptFileIdPrefix}:${e.name}`,
128
name: e.name,
129
value: e.uri,
130
})));
131
}
132
133
const chatVariables = makeCollection([
134
{ name: 'prompt:code-review.prompt.md', uri: URI.file('/workspace/.github/prompts/code-review.prompt.md') },
135
{ name: 'my-skill', uri: URI.file('/workspace/.github/skills/my-skill/SKILL.md') },
136
]);
137
138
test('returns undefined for plain text query', () => {
139
expect(parseSlashCommand('just a question', chatVariables)).toBeUndefined();
140
});
141
142
test('returns undefined when slash command does not match any prompt file', () => {
143
expect(parseSlashCommand('/unknown arg', chatVariables)).toBeUndefined();
144
});
145
146
test('returns undefined for empty query', () => {
147
expect(parseSlashCommand('', chatVariables)).toBeUndefined();
148
});
149
150
test('matches prompt file and returns command and empty args', () => {
151
const result = parseSlashCommand('/code-review', chatVariables);
152
expect(result).toBeDefined();
153
expect(result!.command).toBe('code-review');
154
expect(result!.args).toBe('');
155
expect(result!.promptFile).toEqual({ name: 'prompt:code-review.prompt.md', id: 'code-review' });
156
});
157
158
test('matches prompt file and returns parsed args', () => {
159
const result = parseSlashCommand('/code-review src/foo.ts --strict', chatVariables);
160
expect(result).toBeDefined();
161
expect(result!.command).toBe('code-review');
162
expect(result!.args).toBe('src/foo.ts --strict');
163
});
164
165
test('matches skill file', () => {
166
const result = parseSlashCommand('/my-skill do something', chatVariables);
167
expect(result).toBeDefined();
168
expect(result!.command).toBe('my-skill');
169
expect(result!.promptFile).toEqual({ name: 'my-skill', id: 'my-skill' });
170
expect(result!.args).toBe('do something');
171
});
172
173
test('returns the matched variable', () => {
174
const result = parseSlashCommand('/code-review', chatVariables);
175
expect(result).toBeDefined();
176
expect(URI.isUri(result!.variable.value)).toBe(true);
177
expect(result!.variable.reference.name).toBe('prompt:code-review.prompt.md');
178
});
179
180
test('handles leading whitespace', () => {
181
const result = parseSlashCommand(' /code-review', chatVariables);
182
expect(result).toBeDefined();
183
expect(result!.command).toBe('code-review');
184
});
185
186
test('trims trailing whitespace from args', () => {
187
const result = parseSlashCommand('/code-review arg ', chatVariables);
188
expect(result!.args).toBe('arg');
189
});
190
191
test('skips non-prompt-file references', () => {
192
const mixed = new ChatVariablesCollection([
193
{ id: 'vscode.instructions.file:inst', name: 'inst', value: URI.file('/workspace/inst.md') },
194
{ id: `${PromptFileIdPrefix}:prompt:review.prompt.md`, name: 'prompt:review.prompt.md', value: URI.file('/workspace/review.prompt.md') },
195
]);
196
const result = parseSlashCommand('/review', mixed);
197
expect(result).toBeDefined();
198
expect(result!.command).toBe('review');
199
});
200
201
test('returns undefined with empty collection', () => {
202
expect(parseSlashCommand('/code-review', new ChatVariablesCollection([]))).toBeUndefined();
203
});
204
});
205
206