Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliPromptResolver.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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
import { MockFileSystemService } from '../../../../../platform/filesystem/node/test/mockFileSystemService';
8
import { IIgnoreService, NullIgnoreService } from '../../../../../platform/ignore/common/ignoreService';
9
import { ILogService } from '../../../../../platform/log/common/logService';
10
import { NullWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';
11
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
12
import { DisposableStore } from '../../../../../util/vs/base/common/lifecycle';
13
import { URI } from '../../../../../util/vs/base/common/uri';
14
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
15
import { IVSCodeExtensionContext } from '../../../../../platform/extContext/common/extensionContext';
16
import { MockExtensionContext } from '../../../../../platform/test/node/extensionContext';
17
import { createExtensionUnitTestingServices } from '../../../../test/node/services';
18
import { TestChatRequest } from '../../../../test/node/testHelpers';
19
import { IWorkspaceInfo } from '../../../common/workspaceInfo';
20
import { ICopilotCLIImageSupport } from '../copilotCLIImageSupport';
21
import { CopilotCLIPromptResolver } from '../copilotcliPromptResolver';
22
import { MockSkillLocations, NullICopilotCLIImageSupport } from './testHelpers';
23
24
// Mock generateUserPrompt to avoid TSX rendering complexity in unit tests
25
vi.mock('../../../../prompts/node/agent/copilotCLIPrompt', () => ({
26
generateUserPrompt: vi.fn(async (_request: unknown, prompt: string | undefined, _variables: unknown, _instantiationService: unknown) => prompt ?? ''),
27
}));
28
29
const noopWorkspaceInfo: IWorkspaceInfo = {
30
folder: undefined,
31
repository: undefined,
32
worktree: undefined,
33
worktreeProperties: undefined,
34
};
35
36
function makePromptFileReference(uri: URI) {
37
return {
38
id: 'vscode.prompt.file',
39
value: uri,
40
name: uri.fsPath,
41
};
42
}
43
44
describe('CopilotCLIPromptResolver', () => {
45
let disposables: DisposableStore;
46
let resolver: CopilotCLIPromptResolver;
47
let skillLocations: MockSkillLocations;
48
let logService: ILogService;
49
let instantiationService: IInstantiationService;
50
51
beforeEach(() => {
52
disposables = new DisposableStore();
53
const services = disposables.add(createExtensionUnitTestingServices());
54
const accessor = disposables.add(services.createTestingAccessor());
55
logService = accessor.get(ILogService);
56
instantiationService = accessor.get(IInstantiationService);
57
skillLocations = new MockSkillLocations();
58
});
59
60
afterEach(() => {
61
disposables.dispose();
62
});
63
64
function createResolver(overrideSkillLocations?: MockSkillLocations, overrideExtensionContext?: IVSCodeExtensionContext) {
65
const imageSupport = new NullICopilotCLIImageSupport();
66
const workspaceService = new NullWorkspaceService();
67
const ignoreService = new NullIgnoreService();
68
const fileSystemService = new MockFileSystemService();
69
const extensionContext = overrideExtensionContext ?? new MockExtensionContext() as unknown as IVSCodeExtensionContext;
70
return new CopilotCLIPromptResolver(
71
imageSupport as unknown as ICopilotCLIImageSupport,
72
logService,
73
fileSystemService,
74
workspaceService,
75
instantiationService,
76
ignoreService as unknown as IIgnoreService,
77
overrideSkillLocations ?? skillLocations,
78
extensionContext,
79
);
80
}
81
82
describe('resolvePrompt', () => {
83
it('returns the prompt and empty attachments for a basic request with no references', async () => {
84
resolver = createResolver();
85
const request = new TestChatRequest('hello world');
86
const result = await resolver.resolvePrompt(request, undefined, [], noopWorkspaceInfo, [], CancellationToken.None);
87
expect(result.prompt).toBe('hello world');
88
expect(result.attachments).toHaveLength(0);
89
expect(result.references).toHaveLength(0);
90
});
91
92
it('uses the provided prompt override instead of request.prompt', async () => {
93
resolver = createResolver();
94
const request = new TestChatRequest('original prompt');
95
const result = await resolver.resolvePrompt(request, 'override prompt', [], noopWorkspaceInfo, [], CancellationToken.None);
96
expect(result.prompt).toBe('override prompt');
97
});
98
99
it('cancellation token: returns empty result when cancelled', async () => {
100
resolver = createResolver();
101
const request = new TestChatRequest('hello');
102
const cancelledToken = CancellationToken.Cancelled;
103
const result = await resolver.resolvePrompt(request, undefined, [], noopWorkspaceInfo, [], cancelledToken);
104
expect(result.attachments).toHaveLength(0);
105
expect(result.references).toHaveLength(0);
106
});
107
});
108
109
describe('skill prompt file filtering', () => {
110
it('excludes prompt file references that are within known skill locations', async () => {
111
const skillsDir = URI.file('/home/user/.skills');
112
const skillFile = URI.joinPath(skillsDir, 'my-skill.prompt.md');
113
skillLocations = new MockSkillLocations([skillsDir]);
114
resolver = createResolver(skillLocations);
115
116
const request = new TestChatRequest('use the skill', [makePromptFileReference(skillFile)]);
117
const result = await resolver.resolvePrompt(request, undefined, [], noopWorkspaceInfo, [], CancellationToken.None);
118
119
// The prompt file is within the known skill location, so it should not appear in references
120
expect(result.references).toHaveLength(0);
121
expect(result.attachments).toHaveLength(0);
122
});
123
124
it('includes prompt file references that are NOT within known skill locations', async () => {
125
const skillsDir = URI.file('/home/user/.skills');
126
const nonSkillPromptFile = URI.file('/workspace/some-other.prompt.md');
127
skillLocations = new MockSkillLocations([skillsDir]);
128
resolver = createResolver(skillLocations);
129
130
const request = new TestChatRequest('use a prompt file', [makePromptFileReference(nonSkillPromptFile)]);
131
const result = await resolver.resolvePrompt(request, undefined, [], noopWorkspaceInfo, [], CancellationToken.None);
132
133
// The prompt file is NOT in a skill location, so it should appear in references
134
expect(result.references).toHaveLength(1);
135
expect((result.references[0].value as URI).fsPath).toBe(nonSkillPromptFile.fsPath);
136
});
137
138
it('excludes prompt file when it is in a subdirectory of a known skill location', async () => {
139
const skillsDir = URI.file('/home/user/.skills');
140
const nestedSkillFile = URI.joinPath(skillsDir, 'subdir', 'nested.prompt.md');
141
skillLocations = new MockSkillLocations([skillsDir]);
142
resolver = createResolver(skillLocations);
143
144
const request = new TestChatRequest('use nested skill', [makePromptFileReference(nestedSkillFile)]);
145
const result = await resolver.resolvePrompt(request, undefined, [], noopWorkspaceInfo, [], CancellationToken.None);
146
147
expect(result.references).toHaveLength(0);
148
});
149
150
it('includes prompt file when no skill locations are configured', async () => {
151
skillLocations = new MockSkillLocations([]);
152
resolver = createResolver(skillLocations);
153
154
const promptFile = URI.file('/workspace/my.prompt.md');
155
const request = new TestChatRequest('use prompt', [makePromptFileReference(promptFile)]);
156
const result = await resolver.resolvePrompt(request, undefined, [], noopWorkspaceInfo, [], CancellationToken.None);
157
158
// No skill locations match, so prompt file goes through the full pipeline
159
expect(result.references).toHaveLength(1);
160
});
161
162
it('excludes plan.prompt.md when it is in the prompts directory that is a parent of the extension', async () => {
163
skillLocations = new MockSkillLocations([]);
164
const extensionContext = new MockExtensionContext() as unknown as IVSCodeExtensionContext;
165
resolver = createResolver(skillLocations, extensionContext);
166
167
// The condition checks isEqualOrParent(extensionUri, directory), meaning
168
// directory must be a parent of or equal to extensionUri.
169
// extensionUri = /mock-extension, so place plan.prompt.md at /prompts/plan.prompt.md
170
// where directory = / which IS a parent of /mock-extension.
171
// But path.basename must be 'prompts', so we need /mock-extension/prompts as parent,
172
// which means extensionUri must be under that. Construct extensionUri accordingly.
173
const prompts = URI.file('/test-ext/prompts');
174
// Override extensionUri to be a child of /test-ext/prompts
175
(extensionContext as any).extensionUri = URI.joinPath(prompts, 'inner-ext');
176
resolver = createResolver(skillLocations, extensionContext);
177
178
const planPromptFile = URI.joinPath(prompts, 'plan.prompt.md');
179
const request = new TestChatRequest('implement this', [makePromptFileReference(planPromptFile)]);
180
const result = await resolver.resolvePrompt(request, undefined, [], noopWorkspaceInfo, [], CancellationToken.None);
181
182
// plan.prompt.md from a prompts directory that is a parent of extensionUri should be excluded
183
expect(result.references).toHaveLength(0);
184
expect(result.attachments).toHaveLength(0);
185
});
186
187
it('includes plan.prompt.md when it is NOT in the extension prompts directory', async () => {
188
skillLocations = new MockSkillLocations([]);
189
const extensionContext = new MockExtensionContext() as unknown as IVSCodeExtensionContext;
190
resolver = createResolver(skillLocations, extensionContext);
191
192
const planPromptFile = URI.file('/workspace/plan.prompt.md');
193
const request = new TestChatRequest('implement this', [makePromptFileReference(planPromptFile)]);
194
const result = await resolver.resolvePrompt(request, undefined, [], noopWorkspaceInfo, [], CancellationToken.None);
195
196
// plan.prompt.md from a workspace directory (not extension prompts dir) should be included
197
expect(result.references).toHaveLength(1);
198
});
199
});
200
});
201
202