Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/vscode-node/test/promptVariablesService.spec.ts
13405 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 { tmpdir } from 'os';
7
import { join } from 'path';
8
import { beforeEach, describe, expect, test } from 'vitest';
9
import type { ChatLanguageModelToolReference, ChatPromptReference } from 'vscode';
10
import { IChatDebugFileLoggerService, NullChatDebugFileLoggerService } from '../../../../platform/chat/common/chatDebugFileLoggerService';
11
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
12
import { MockExtensionContext } from '../../../../platform/test/node/extensionContext';
13
import { ITestingServicesAccessor, TestingServiceCollection } from '../../../../platform/test/node/services';
14
import { URI } from '../../../../util/vs/base/common/uri';
15
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
16
import { Uri } from '../../../../vscodeTypes';
17
import { createExtensionUnitTestingServices } from '../../../test/node/services';
18
import { PromptVariablesServiceImpl } from '../promptVariablesService';
19
20
class MockChatDebugFileLoggerService extends NullChatDebugFileLoggerService {
21
private readonly _sessionDirs = new Map<string, URI>();
22
23
setSessionDir(sessionId: string, dir: URI): void {
24
this._sessionDirs.set(sessionId, dir);
25
}
26
27
override getSessionDir(sessionId: string): URI | undefined {
28
return this._sessionDirs.get(sessionId);
29
}
30
}
31
32
function createServicesWithLogger(mockLogger?: MockChatDebugFileLoggerService): { testingServiceCollection: TestingServiceCollection; mockLogger: MockChatDebugFileLoggerService } {
33
const logger = mockLogger ?? new MockChatDebugFileLoggerService();
34
const testingServiceCollection = createExtensionUnitTestingServices();
35
// Provide a globalStorageUri so VSCODE_USER_PROMPTS_FOLDER can resolve
36
const ctx = new MockExtensionContext(join(tmpdir(), 'copilot-test-globalStorage'));
37
testingServiceCollection.define(IVSCodeExtensionContext, ctx as any);
38
testingServiceCollection.define(IChatDebugFileLoggerService, logger);
39
return { testingServiceCollection, mockLogger: logger };
40
}
41
42
describe('PromptVariablesServiceImpl', () => {
43
let accessor: ITestingServicesAccessor;
44
let service: PromptVariablesServiceImpl;
45
46
beforeEach(() => {
47
const testingServiceCollection = createExtensionUnitTestingServices();
48
accessor = testingServiceCollection.createTestingAccessor();
49
// Create the service via DI so its dependencies (fs + workspace) come from the test container
50
service = accessor.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);
51
});
52
53
test('replaces variable ranges with link markdown', async () => {
54
const original = 'Start #VARIABLE1 #VARIABLE2 End #VARIABLE3';
55
56
const variables: ChatPromptReference[] = [];
57
['#VARIABLE1', '#VARIABLE2', '#VARIABLE3'].forEach((varName, index) => {
58
const start = original.indexOf(varName);
59
const end = start + varName.length;
60
variables.push({
61
id: 'file' + index,
62
name: 'file' + index,
63
value: Uri.file(`/virtual/workspace/sample${index}.txt`),
64
range: [start, end]
65
});
66
});
67
68
const { message } = await service.resolveVariablesInPrompt(original, variables);
69
expect(message).toBe('Start [#file0](#file0-context) [#file1](#file1-context) End [#file2](#file2-context)');
70
});
71
72
test('replaces multiple tool references (deduplicating identical ranges) in reverse-sorted order', async () => {
73
// message with two target substrings we will replace: TOOLX and TOOLY
74
const message = 'Call #TOOLX then maybe #TOOLY finally done';
75
76
const toolRefs: ChatLanguageModelToolReference[] = [];
77
['#TOOLX', '#TOOLY'].forEach((toolRef, index) => {
78
const start = message.indexOf(toolRef);
79
const end = start + toolRef.length;
80
toolRefs.push({
81
name: 'tool' + index,
82
range: [start, end]
83
});
84
toolRefs.push({
85
name: 'tool' + index + 'Duplicate',
86
range: [start, end]
87
});
88
89
});
90
91
const rewritten = await service.resolveToolReferencesInPrompt(message, toolRefs);
92
// Expect TOOLY replaced, then TOOLX replaced; duplicates ignored
93
expect(rewritten).toBe('Call \'tool0\' then maybe \'tool1\' finally done');
94
});
95
96
test('handles no-op when no variables or tool references', async () => {
97
const msg = 'Nothing to change';
98
const { message: out } = await service.resolveVariablesInPrompt(msg, []);
99
const rewritten = await service.resolveToolReferencesInPrompt(out, []);
100
expect(rewritten).toBe(msg);
101
});
102
103
describe('buildTemplateVariablesContext', () => {
104
test('returns empty string when no session id and no debug target session ids are given', () => {
105
// Default NullChatDebugFileLoggerService returns undefined for every getSessionDir,
106
// so VSCODE_TARGET_SESSION_LOG resolves to undefined.
107
// VSCODE_USER_PROMPTS_FOLDER always resolves, so build a fresh service with the default null logger.
108
const { testingServiceCollection } = createServicesWithLogger();
109
const acc = testingServiceCollection.createTestingAccessor();
110
const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);
111
const result = svc.buildTemplateVariablesContext(undefined);
112
// Only VSCODE_USER_PROMPTS_FOLDER should be present
113
expect(result).toContain('VSCODE_USER_PROMPTS_FOLDER');
114
expect(result).not.toContain('VSCODE_TARGET_SESSION_LOG');
115
});
116
117
test('resolves single sessionId to session log path', () => {
118
const mockLogger = new MockChatDebugFileLoggerService();
119
mockLogger.setSessionDir('session-1', URI.file('/logs/session-1'));
120
const { testingServiceCollection } = createServicesWithLogger(mockLogger);
121
const acc = testingServiceCollection.createTestingAccessor();
122
const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);
123
124
const result = svc.buildTemplateVariablesContext('session-1');
125
expect(result).toContain('VSCODE_TARGET_SESSION_LOG');
126
expect(result).toContain('/logs/session-1');
127
});
128
129
test('prioritizes debugTargetSessionIds over sessionId', () => {
130
const mockLogger = new MockChatDebugFileLoggerService();
131
mockLogger.setSessionDir('session-1', URI.file('/logs/session-1'));
132
mockLogger.setSessionDir('target-1', URI.file('/logs/target-1'));
133
const { testingServiceCollection } = createServicesWithLogger(mockLogger);
134
const acc = testingServiceCollection.createTestingAccessor();
135
const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);
136
137
const result = svc.buildTemplateVariablesContext('session-1', ['target-1']);
138
expect(result).toContain('/logs/target-1');
139
// session-1 should NOT appear because debugTargetSessionIds takes precedence
140
expect(result).not.toContain('/logs/session-1');
141
});
142
143
test('formats multiple debugTargetSessionIds as comma-separated paths', () => {
144
const mockLogger = new MockChatDebugFileLoggerService();
145
mockLogger.setSessionDir('target-1', URI.file('/logs/target-1'));
146
mockLogger.setSessionDir('target-2', URI.file('/logs/target-2'));
147
const { testingServiceCollection } = createServicesWithLogger(mockLogger);
148
const acc = testingServiceCollection.createTestingAccessor();
149
const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);
150
151
const result = svc.buildTemplateVariablesContext(undefined, ['target-1', 'target-2']);
152
expect(result).toContain('VSCODE_TARGET_SESSION_LOG');
153
expect(result).toContain('/logs/target-1');
154
expect(result).toContain('/logs/target-2');
155
// Both paths joined with comma
156
expect(result).toMatch(/\/logs\/target-1, \/logs\/target-2/);
157
});
158
159
test('skips debugTargetSessionIds whose session dirs are missing', () => {
160
const mockLogger = new MockChatDebugFileLoggerService();
161
// Only target-2 has a session dir; target-1 does not
162
mockLogger.setSessionDir('target-2', URI.file('/logs/target-2'));
163
const { testingServiceCollection } = createServicesWithLogger(mockLogger);
164
const acc = testingServiceCollection.createTestingAccessor();
165
const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);
166
167
const result = svc.buildTemplateVariablesContext(undefined, ['target-1', 'target-2']);
168
expect(result).toContain('/logs/target-2');
169
expect(result).not.toContain('target-1');
170
});
171
172
test('includes VSCODE_TARGET_SESSION_LOG with empty value when all debugTargetSessionIds have missing dirs', () => {
173
const mockLogger = new MockChatDebugFileLoggerService();
174
// No session dirs set at all
175
const { testingServiceCollection } = createServicesWithLogger(mockLogger);
176
const acc = testingServiceCollection.createTestingAccessor();
177
const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);
178
179
const result = svc.buildTemplateVariablesContext(undefined, ['no-such-session']);
180
// The resolver returns '' (empty string) when all dirs are missing, not undefined,
181
// so the variable is still present in the output with an empty value.
182
expect(result).toContain('VSCODE_TARGET_SESSION_LOG');
183
expect(result).toMatch(/VSCODE_TARGET_SESSION_LOG:\s*$/m);
184
});
185
186
test('includes VSCODE_USER_PROMPTS_FOLDER derived from global storage URI', () => {
187
const { testingServiceCollection } = createServicesWithLogger();
188
const acc = testingServiceCollection.createTestingAccessor();
189
const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);
190
191
const result = svc.buildTemplateVariablesContext(undefined);
192
expect(result).toContain('VSCODE_USER_PROMPTS_FOLDER');
193
// The path should end with /prompts
194
expect(result).toMatch(/prompts/);
195
});
196
197
test('returns empty string when sessionId has no session dir and no debugTargetSessionIds', () => {
198
const mockLogger = new MockChatDebugFileLoggerService();
199
// session-missing has no dir registered
200
const { testingServiceCollection } = createServicesWithLogger(mockLogger);
201
const acc = testingServiceCollection.createTestingAccessor();
202
const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);
203
204
const result = svc.buildTemplateVariablesContext('session-missing');
205
// VSCODE_USER_PROMPTS_FOLDER still resolves
206
expect(result).toContain('VSCODE_USER_PROMPTS_FOLDER');
207
// But session log should not be present
208
expect(result).not.toContain('VSCODE_TARGET_SESSION_LOG');
209
});
210
});
211
});
212
213