Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/test/web.test.ts
13389 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 { ServiceCollection } from '../../platform/instantiation/common/serviceCollection.js';
7
import { ILogService } from '../../platform/log/common/log.js';
8
import { IBrowserMainWorkbench } from '../../workbench/browser/web.main.js';
9
import { Workbench as SessionsWorkbench } from '../browser/workbench.js';
10
import { SessionsBrowserMain } from '../browser/web.main.js';
11
import { Emitter, Event } from '../../base/common/event.js';
12
import { CancellationToken } from '../../base/common/cancellation.js';
13
import { IObservable, observableValue } from '../../base/common/observable.js';
14
import { ChatEntitlement, IChatEntitlementService, IChatSentiment } from '../../workbench/services/chat/common/chatEntitlementService.js';
15
import { IDefaultAccountService } from '../../platform/defaultAccount/common/defaultAccount.js';
16
import { IDefaultAccount, IDefaultAccountAuthenticationProvider, ICopilotTokenInfo, IPolicyData } from '../../base/common/defaultAccount.js';
17
import { IChatAgentService, IChatAgentData, IChatAgentImplementation } from '../../workbench/contrib/chat/common/participants/chatAgents.js';
18
import { ChatAgentLocation, ChatModeKind } from '../../workbench/contrib/chat/common/constants.js';
19
import { ExtensionIdentifier } from '../../platform/extensions/common/extensions.js';
20
import { IStorageService, StorageScope, StorageTarget } from '../../platform/storage/common/storage.js';
21
import { URI } from '../../base/common/uri.js';
22
import { Disposable } from '../../base/common/lifecycle.js';
23
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../workbench/common/contributions.js';
24
import { IChatProgress } from '../../workbench/contrib/chat/common/chatService/chatService.js';
25
import { IChatSessionsService, IChatSessionItem, IChatSessionFileChange, ChatSessionStatus, IChatSessionHistoryItem, IChatSessionItemsDelta } from '../../workbench/contrib/chat/common/chatSessionsService.js';
26
import { IGitService, IGitExtensionDelegate, IGitRepository } from '../../workbench/contrib/git/common/gitService.js';
27
import { IFileService } from '../../platform/files/common/files.js';
28
import { ITerminalService } from '../../workbench/contrib/terminal/browser/terminal.js';
29
import { ITerminalBackend, ITerminalBackendRegistry, IProcessReadyEvent, IProcessProperty, ProcessPropertyType, TerminalExtensions, ITerminalProcessOptions, IShellLaunchConfig } from '../../platform/terminal/common/terminal.js';
30
import { IProcessEnvironment } from '../../base/common/platform.js';
31
import { Registry } from '../../platform/registry/common/platform.js';
32
import { InMemoryFileSystemProvider } from '../../platform/files/common/inMemoryFilesystemProvider.js';
33
import { VSBuffer } from '../../base/common/buffer.js';
34
import { SyncDescriptor } from '../../platform/instantiation/common/descriptors.js';
35
import { getSingletonServiceDescriptors } from '../../platform/instantiation/common/extensions.js';
36
import { ServiceIdentifier } from '../../platform/instantiation/common/instantiation.js';
37
import { IWorkbench } from '../../workbench/browser/web.api.js';
38
import { isEqual } from '../../base/common/resources.js';
39
40
/**
41
* Mock files pre-seeded in the in-memory file system. These match the
42
* paths in EXISTING_MOCK_FILES and are used by the ChatEditingService
43
* to compute before/after diffs.
44
*/
45
const MOCK_FS_FILES: Record<string, string> = {
46
'/mock-repo/src/index.ts': 'export function main() {\n\tconsole.log("Hello from mock repo");\n}\n',
47
'/mock-repo/src/utils.ts': 'export function add(a: number, b: number): number {\n\treturn a + b;\n}\n',
48
'/mock-repo/package.json': '{\n\t"name": "mock-repo",\n\t"version": "1.0.0"\n}\n',
49
'/mock-repo/README.md': '# Mock Repository\n\nThis is a mock repository for E2E testing.\n',
50
};
51
52
/**
53
* Register the mock-fs:// file system provider directly in the workbench
54
* so it is available immediately at startup — before any service
55
* (SnippetsService, PromptFilesLocator, MCP, etc.) tries to resolve
56
* files inside the workspace folder.
57
*/
58
function registerMockFileSystemProvider(serviceCollection: ServiceCollection): void {
59
const fileService = serviceCollection.get(IFileService) as IFileService;
60
const provider = new InMemoryFileSystemProvider();
61
fileService.registerProvider('mock-fs', provider);
62
63
// Pre-populate the files so ChatEditingService can read originals for diffs
64
for (const [filePath, content] of Object.entries(MOCK_FS_FILES)) {
65
const uri = URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: filePath });
66
fileService.writeFile(uri, VSBuffer.fromString(content));
67
}
68
console.log('[Sessions Web Test] Registered mock-fs:// provider with pre-seeded files');
69
}
70
71
const MOCK_ACCOUNT: IDefaultAccount = {
72
authenticationProvider: { id: 'github', name: 'GitHub (Mock)', enterprise: false },
73
accountName: 'e2e-test-user',
74
sessionId: 'mock-session-1',
75
enterprise: false,
76
};
77
78
/**
79
* Mock implementation of IChatEntitlementService that makes the Sessions
80
* window think the user is signed in with a Free Copilot plan.
81
*/
82
class MockChatEntitlementService implements IChatEntitlementService {
83
84
declare readonly _serviceBrand: undefined;
85
86
readonly onDidChangeEntitlement = Event.None;
87
readonly onDidChangeQuotaExceeded = Event.None;
88
readonly onDidChangeQuotaRemaining = Event.None;
89
readonly onDidChangeSentiment = Event.None;
90
readonly onDidChangeAnonymous = Event.None;
91
92
readonly entitlement = ChatEntitlement.Free;
93
readonly entitlementObs: IObservable<ChatEntitlement> = observableValue('entitlement', ChatEntitlement.Free);
94
95
readonly previewFeaturesDisabled = false;
96
readonly clientByokEnabled = false;
97
readonly organisations: string[] | undefined = undefined;
98
readonly isInternal = false;
99
readonly sku = 'free';
100
readonly copilotTrackingId = 'mock-tracking-id';
101
102
readonly quotas = {};
103
104
readonly sentiment: IChatSentiment = { completed: true, registered: true };
105
readonly sentimentObs: IObservable<IChatSentiment> = observableValue('sentiment', { completed: true, registered: true });
106
107
readonly anonymous = false;
108
readonly anonymousObs: IObservable<boolean> = observableValue('anonymous', false);
109
110
markAnonymousRateLimited(): void { }
111
setForceHidden(_hidden: boolean): void { }
112
async update(_token: CancellationToken): Promise<void> { }
113
}
114
115
/**
116
* Mock implementation of IDefaultAccountService that returns a fake
117
* signed-in account so the "Sign In" button in the sidebar is hidden.
118
*/
119
class MockDefaultAccountService implements IDefaultAccountService {
120
121
declare readonly _serviceBrand: undefined;
122
123
readonly onDidChangeDefaultAccount = Event.None;
124
readonly onDidChangePolicyData = Event.None;
125
readonly policyData: IPolicyData | null = null;
126
readonly currentDefaultAccount: IDefaultAccount | null = MOCK_ACCOUNT;
127
readonly copilotTokenInfo: ICopilotTokenInfo | null = null;
128
readonly onDidChangeCopilotTokenInfo = Event.None;
129
130
async getDefaultAccount(): Promise<IDefaultAccount | null> { return MOCK_ACCOUNT; }
131
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { return MOCK_ACCOUNT.authenticationProvider; }
132
setDefaultAccountProvider(): void { }
133
async refresh(): Promise<IDefaultAccount | null> { return MOCK_ACCOUNT; }
134
async signIn(): Promise<IDefaultAccount | null> { return MOCK_ACCOUNT; }
135
async signOut(): Promise<void> { }
136
}
137
138
// ---------------------------------------------------------------------------
139
// Mock chat responses and file changes
140
// ---------------------------------------------------------------------------
141
142
/**
143
* Paths that exist in the mock-fs file store pre-seeded by the mock extension.
144
* Used to determine whether a textEdit should replace file content (existing)
145
* or insert into an empty buffer (new file), so the real ChatEditingService
146
* computes meaningful before/after diffs.
147
*/
148
const EXISTING_MOCK_FILES = new Set(['/mock-repo/src/index.ts', '/mock-repo/src/utils.ts', '/mock-repo/package.json', '/mock-repo/README.md']);
149
150
interface MockFileEdit {
151
uri: URI;
152
content: string;
153
}
154
155
interface MockResponse {
156
text: string;
157
fileEdits?: MockFileEdit[];
158
}
159
160
/**
161
* Emit textEdit progress items for each file edit using the real ChatModel
162
* pipeline. Existing files use a full-file replacement range so the real
163
* ChatEditingService computes an accurate diff. New files use an
164
* insert-at-beginning range.
165
*/
166
function emitFileEdits(fileEdits: MockFileEdit[], progress: (parts: IChatProgress[]) => void): void {
167
for (const edit of fileEdits) {
168
const isExistingFile = EXISTING_MOCK_FILES.has(edit.uri.path);
169
const range = isExistingFile
170
? { startLineNumber: 1, startColumn: 1, endLineNumber: 99999, endColumn: 1 }
171
: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 };
172
console.log(`[Sessions Web Test] Emitting textEdit for ${edit.uri.toString()} (existing: ${isExistingFile}, range: ${range.startLineNumber}-${range.endLineNumber})`);
173
progress([{
174
kind: 'textEdit',
175
uri: edit.uri,
176
edits: [{ range, text: edit.content }],
177
done: true,
178
}]);
179
}
180
}
181
182
/**
183
* Return canned response text and file edits keyed by user message keywords.
184
*
185
* File edits target URIs in the mock-fs:// filesystem. Edits for existing
186
* files produce real diffs (original content from mock-fs → new content here).
187
* Edits for new files produce "file created" entries.
188
*/
189
function getMockResponseWithEdits(message: string): MockResponse {
190
if (/build|compile|create/i.test(message)) {
191
return {
192
text: 'I\'ll help you build the project. Here are the changes:',
193
fileEdits: [
194
{
195
// Modify existing file — adds build import + call
196
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/src/index.ts' }),
197
content: 'import { build } from "./build";\n\nexport function main() {\n\tconsole.log("Hello from mock repo");\n\tbuild();\n}\n',
198
},
199
{
200
// New file — creates build script
201
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/src/build.ts' }),
202
content: 'export async function build() {\n\tconsole.log("Building...");\n\tconsole.log("Build complete!");\n}\n',
203
},
204
{
205
// Modify existing file — adds build script
206
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/package.json' }),
207
content: '{\n\t"name": "mock-repo",\n\t"version": "1.0.0",\n\t"scripts": {\n\t\t"build": "node src/build.ts"\n\t}\n}\n',
208
},
209
],
210
};
211
}
212
if (/fix|bug/i.test(message)) {
213
return {
214
text: 'I found the issue and applied the fix. The input validation has been added.',
215
fileEdits: [
216
{
217
// Modify existing file — adds input validation
218
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/src/utils.ts' }),
219
content: 'export function add(a: number, b: number): number {\n\tif (typeof a !== "number" || typeof b !== "number") {\n\t\tthrow new TypeError("Both arguments must be numbers");\n\t}\n\treturn a + b;\n}\n',
220
},
221
],
222
};
223
}
224
if (/explain|describe/i.test(message)) {
225
return {
226
text: 'This project has a simple structure with a main entry point and utility functions.',
227
};
228
}
229
return {
230
text: 'I understand your request. Let me work on that.\n\n1. Review the codebase\n2. Make changes\n3. Run tests',
231
};
232
}
233
234
// ---------------------------------------------------------------------------
235
// Workbench contribution — registers mock chat agent and pre-seeds folder
236
// ---------------------------------------------------------------------------
237
238
class MockChatAgentContribution extends Disposable implements IWorkbenchContribution {
239
240
static readonly ID = 'sessions.test.mockChatAgent';
241
242
private readonly _sessionItems: IChatSessionItem[] = [];
243
private readonly _itemsChangedEmitter = new Emitter<IChatSessionItemsDelta>();
244
private readonly _sessionHistory = new Map<string, IChatSessionHistoryItem[]>();
245
private _worktreeCounter = 0;
246
247
constructor(
248
@IChatAgentService private readonly chatAgentService: IChatAgentService,
249
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
250
@ITerminalService private readonly terminalService: ITerminalService,
251
) {
252
super();
253
this._register(this._itemsChangedEmitter);
254
this.registerMockAgents();
255
this.registerMockSessionProvider();
256
this.registerMockTerminalBackend();
257
}
258
259
/**
260
* Track a session for sidebar display and history re-opening.
261
*
262
* Populates `IChatSessionItem.changes` with file change metadata so the
263
* ChangesViewPane can render them for background (copilotcli) sessions.
264
* Background sessions read changes from `IAgentSessionsService.model`
265
* which flows through from `IChatSessionItemController.items`.
266
*/
267
private addSessionItem(resource: URI, message: string, responseText: string, fileEdits?: MockFileEdit[]): void {
268
const key = resource.toString();
269
const now = Date.now();
270
271
// Store conversation history for this session (needed for re-opening)
272
if (!this._sessionHistory.has(key)) {
273
this._sessionHistory.set(key, []);
274
}
275
this._sessionHistory.get(key)!.push(
276
{ type: 'request', prompt: message, participant: 'copilot' },
277
{ type: 'response', parts: [{ kind: 'markdownContent', content: { value: responseText, isTrusted: false, supportThemeIcons: false, supportHtml: false } }], participant: 'copilot' },
278
);
279
280
// Build file changes for the session list (used by ChangesViewPane for background sessions)
281
const changes: IChatSessionFileChange[] | undefined = fileEdits?.map(edit => ({
282
modifiedUri: edit.uri,
283
insertions: edit.content.split('\n').length,
284
deletions: EXISTING_MOCK_FILES.has(edit.uri.path) ? 1 : 0,
285
}));
286
287
// Add or update session in list
288
const existingIndex = this._sessionItems.findIndex(s => isEqual(s.resource, resource));
289
let addedOrUpdated = existingIndex !== -1 ? { ...this._sessionItems[existingIndex] } : undefined;
290
if (addedOrUpdated) {
291
addedOrUpdated.timing = { ...addedOrUpdated.timing, lastRequestStarted: now, lastRequestEnded: now };
292
if (changes) {
293
addedOrUpdated.changes = changes;
294
}
295
this._sessionItems[existingIndex] = addedOrUpdated;
296
} else {
297
addedOrUpdated = {
298
resource,
299
label: message.slice(0, 50) || 'Mock Session',
300
status: ChatSessionStatus.Completed,
301
timing: { created: now, lastRequestStarted: now, lastRequestEnded: now },
302
metadata: { worktreePath: `/mock-worktrees/session-${++this._worktreeCounter}` },
303
...(changes ? { changes } : {}),
304
};
305
this._sessionItems.push(addedOrUpdated);
306
}
307
308
if (addedOrUpdated) {
309
this._itemsChangedEmitter.fire({ addedOrUpdated: [addedOrUpdated] });
310
}
311
}
312
313
private registerMockAgents(): void {
314
const agentIds = ['copilotcli', 'copilot-cloud-agent'];
315
const extensionId = new ExtensionIdentifier('vscode.sessions-e2e-mock');
316
const self = this;
317
318
for (const agentId of agentIds) {
319
const agentData: IChatAgentData = {
320
id: agentId,
321
name: agentId,
322
fullName: `Mock Agent (${agentId})`,
323
description: 'Mock chat agent for E2E testing',
324
extensionId,
325
extensionVersion: '0.0.1',
326
extensionPublisherId: 'vscode',
327
extensionDisplayName: 'Sessions E2E Mock',
328
isDefault: agentId === 'copilotcli',
329
metadata: {},
330
slashCommands: [],
331
locations: [ChatAgentLocation.Chat],
332
modes: [ChatModeKind.Agent],
333
disambiguation: [],
334
};
335
336
const agentImpl: IChatAgentImplementation = {
337
async invoke(request, progress: (parts: IChatProgress[]) => void, _history, _token) {
338
console.log(`[Sessions Web Test] Mock agent "${agentId}" invoked: "${request.message}"`);
339
const response = getMockResponseWithEdits(request.message);
340
341
// Stream the text response
342
progress([{
343
kind: 'markdownContent',
344
content: { value: response.text, isTrusted: false, supportThemeIcons: false, supportHtml: false },
345
}]);
346
347
// Emit file edits through the real ChatModel pipeline so
348
// ChatEditingService computes actual diffs
349
if (response.fileEdits) {
350
emitFileEdits(response.fileEdits, progress);
351
console.log(`[Sessions Web Test] Emitted ${response.fileEdits.length} file edits OK`);
352
}
353
354
self.addSessionItem(request.sessionResource, request.message, response.text, response.fileEdits);
355
return { metadata: { mock: true } };
356
},
357
};
358
359
try {
360
this._register(this.chatAgentService.registerDynamicAgent(agentData, agentImpl));
361
console.log(`[Sessions Web Test] Registered mock agent: ${agentId}`);
362
} catch (err) {
363
console.warn(`[Sessions Web Test] Failed to register agent ${agentId}:`, err);
364
}
365
}
366
}
367
368
private registerMockSessionProvider(): void {
369
const schemes = ['copilotcli', 'copilot-cloud-agent'];
370
const self = this;
371
for (const scheme of schemes) {
372
try {
373
this._register(this.chatSessionsService.registerChatSessionContentProvider(scheme, {
374
async provideChatSessionContent(sessionResource, _token) {
375
const key = sessionResource.toString();
376
// Ensure the history array is stored in _sessionHistory so
377
// addSessionItem pushes into the SAME reference returned here.
378
if (!self._sessionHistory.has(key)) {
379
self._sessionHistory.set(key, []);
380
}
381
const history = self._sessionHistory.get(key)!;
382
console.log(`[Sessions Web Test] Opening session ${key} (${history.length} history items)`);
383
const disposeEmitter = new Emitter<void>();
384
const isComplete = observableValue('isComplete', history.length > 0);
385
return {
386
sessionResource,
387
history,
388
isCompleteObs: isComplete,
389
onWillDispose: disposeEmitter.event,
390
async requestHandler(request, progress, _history, _token) {
391
console.log(`[Sessions Web Test] Session request: "${request.message}"`);
392
const response = getMockResponseWithEdits(request.message);
393
progress([{
394
kind: 'markdownContent',
395
content: { value: response.text, isTrusted: false, supportThemeIcons: false, supportHtml: false },
396
}]);
397
if (response.fileEdits) {
398
emitFileEdits(response.fileEdits, progress);
399
}
400
isComplete.set(true, undefined);
401
},
402
dispose() { disposeEmitter.fire(); disposeEmitter.dispose(); },
403
};
404
},
405
}));
406
407
// Register an item controller so sessions appear in the sidebar list.
408
// Only copilotcli (Background) sessions need real items — the
409
// copilot-cloud-agent controller must return an empty array to
410
// prevent it from overwriting sessions with the wrong providerType
411
// during a full model resolve.
412
const controllerItems = scheme === 'copilotcli' ? this._sessionItems : [];
413
this._register(this.chatSessionsService.registerChatSessionItemController(scheme, {
414
onDidChangeChatSessionItems: this._itemsChangedEmitter.event,
415
get items() { return controllerItems; },
416
async refresh() { /* in-memory, no-op */ },
417
}));
418
419
console.log(`[Sessions Web Test] Registered session provider for scheme: ${scheme}`);
420
} catch (err) {
421
console.warn(`[Sessions Web Test] Failed to register session provider for ${scheme}:`, err);
422
}
423
}
424
}
425
426
private registerMockTerminalBackend(): void {
427
const terminalService = this.terminalService;
428
const backend = this.createMockTerminalBackend();
429
Registry.as<ITerminalBackendRegistry>(TerminalExtensions.Backend).registerTerminalBackend(backend);
430
terminalService.registerProcessSupport(true);
431
console.log('[Sessions Web Test] Registered mock terminal backend');
432
}
433
434
private createMockTerminalBackend(): ITerminalBackend {
435
return {
436
remoteAuthority: undefined,
437
isVirtualProcess: false,
438
isResponsive: true,
439
whenReady: Promise.resolve(),
440
setReady: () => { },
441
onDidRequestDetach: Event.None,
442
attachToProcess: async () => { throw new Error('Not supported'); },
443
attachToRevivedProcess: async () => { throw new Error('Not supported'); },
444
listProcesses: async () => [],
445
getProfiles: async () => [],
446
getDefaultProfile: async () => undefined,
447
getDefaultSystemShell: async () => '/bin/mock-shell',
448
getShellEnvironment: async () => ({}),
449
setTerminalLayoutInfo: async () => { },
450
getTerminalLayoutInfo: async () => undefined,
451
reduceConnectionGraceTime: () => { },
452
requestDetachInstance: () => { },
453
acceptDetachInstanceReply: () => { },
454
persistTerminalState: () => { },
455
createProcess: async (_shellLaunchConfig: IShellLaunchConfig, _cwd: string | URI, _cols: number, _rows: number, _unicodeVersion: string, _env: IProcessEnvironment, _options: ITerminalProcessOptions, _shouldPersist: boolean) => {
456
const onProcessData = new Emitter<string>();
457
const onProcessReady = new Emitter<IProcessReadyEvent>();
458
const onProcessExit = new Emitter<number | undefined>();
459
const onDidChangeHasChildProcesses = new Emitter<boolean>();
460
const onDidChangeProperty = new Emitter<IProcessProperty<ProcessPropertyType>>();
461
462
// Resolve cwd from createProcess arg or shellLaunchConfig
463
const rawCwd = _cwd || _shellLaunchConfig.cwd;
464
const cwd = !rawCwd ? '/' : typeof rawCwd === 'string' ? rawCwd : rawCwd.path;
465
console.log(`[Sessions Web Test] Mock terminal createProcess cwd: '${cwd}' (raw _cwd: '${_cwd}', slc.cwd: '${_shellLaunchConfig.cwd}')`);
466
467
// Fire ready after a microtask so the terminal service can wire up listeners
468
setTimeout(() => {
469
onProcessReady.fire({ pid: 1, cwd, windowsPty: undefined });
470
}, 0);
471
472
return {
473
id: 0,
474
shouldPersist: false,
475
onProcessData: onProcessData.event,
476
onProcessReady: onProcessReady.event,
477
onDidChangeHasChildProcesses: onDidChangeHasChildProcesses.event,
478
onDidChangeProperty: onDidChangeProperty.event,
479
onProcessExit: onProcessExit.event,
480
start: async () => undefined,
481
shutdown: async () => { },
482
input: async () => { },
483
resize: () => { },
484
clearBuffer: () => { },
485
acknowledgeDataEvent: () => { },
486
setUnicodeVersion: async () => { },
487
getInitialCwd: async () => cwd,
488
getCwd: async () => cwd,
489
getLatency: async () => [],
490
processBinary: async () => { },
491
refreshProperty: async (property: ProcessPropertyType) => { throw new Error(`Not supported: ${property}`); },
492
updateProperty: async () => { },
493
clearUnrespondedRequest: () => { },
494
};
495
},
496
getWslPath: async (original: string, _direction: 'unix-to-win' | 'win-to-unix') => original,
497
getEnvironment: async () => ({}),
498
getLatency: async () => [],
499
getPerformanceMarks: () => [],
500
updateTitle: async () => { },
501
updateIcon: async () => { },
502
setNextCommandId: async () => { },
503
restartPtyHost: () => { },
504
installAutoReply: async () => { },
505
uninstallAllAutoReplies: async () => { },
506
onPtyHostUnresponsive: Event.None,
507
onPtyHostResponsive: Event.None,
508
onPtyHostRestart: Event.None,
509
onPtyHostConnected: Event.None,
510
} as unknown as ITerminalBackend;
511
}
512
513
514
}
515
516
// Register the contribution so it runs during workbench startup
517
registerWorkbenchContribution2(MockChatAgentContribution.ID, MockChatAgentContribution, WorkbenchPhase.BlockStartup);
518
519
// ---------------------------------------------------------------------------
520
// MockGitService — resolves immediately instead of waiting 10s for delegate
521
// ---------------------------------------------------------------------------
522
523
class MockGitService implements IGitService {
524
declare readonly _serviceBrand: undefined;
525
readonly repositories: Iterable<IGitRepository> = [];
526
setDelegate(_delegate: IGitExtensionDelegate) { return Disposable.None; }
527
async openRepository(_uri: URI) { return undefined; }
528
}
529
530
// ---------------------------------------------------------------------------
531
// TestSessionsBrowserMain
532
// ---------------------------------------------------------------------------
533
534
/**
535
* Test variant of SessionsBrowserMain that injects mock services
536
* for E2E testing. Mock singletons are patched into the global
537
* singleton registry before `super.open()` so they take effect
538
* during both `BrowserMain.initServices()` and `Workbench.initServices()`.
539
* Original descriptors are restored when the workbench shuts down.
540
*/
541
export class TestSessionsBrowserMain extends SessionsBrowserMain {
542
543
private _savedDescriptors: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [];
544
545
override async open(): Promise<IWorkbench> {
546
// Patch the global singleton registry BEFORE super.open() calls initServices().
547
// getSingletonServiceDescriptors() returns the mutable internal array, so
548
// replacing entries here ensures both BrowserMain and Workbench pick up mocks.
549
const registry = getSingletonServiceDescriptors();
550
const overrides: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [
551
[IChatEntitlementService, new SyncDescriptor(MockChatEntitlementService)],
552
[IDefaultAccountService, new SyncDescriptor(MockDefaultAccountService)],
553
[IGitService, new SyncDescriptor(MockGitService)],
554
];
555
for (const [serviceId, mockDescriptor] of overrides) {
556
const idx = registry.findIndex(([id]) => id === serviceId);
557
if (idx !== -1) {
558
this._savedDescriptors.push([serviceId, registry[idx][1]]);
559
registry[idx] = [serviceId, mockDescriptor];
560
} else {
561
registry.push([serviceId, mockDescriptor]);
562
}
563
}
564
565
const workbench = await super.open();
566
567
// Restore original descriptors now that the workbench has started,
568
// so subsequent tests in the same process are not affected.
569
for (const [serviceId, original] of this._savedDescriptors) {
570
const idx = registry.findIndex(([id]) => id === serviceId);
571
if (idx !== -1) {
572
registry[idx] = [serviceId, original];
573
}
574
}
575
576
return workbench;
577
}
578
579
private preseedFolder(storageService: IStorageService): void {
580
const mockFolderUri = URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo' });
581
const providerId = 'default-copilot';
582
583
// Seed recent workspaces so resolveWorkspace() can hydrate the selection
584
const recentWorkspaces = JSON.stringify([{ uri: mockFolderUri.toJSON(), providerId, checked: true }]);
585
storageService.store('sessions.recentlyPickedWorkspaces', recentWorkspaces, StorageScope.PROFILE, StorageTarget.MACHINE);
586
587
console.log(`[Sessions Web Test] Pre-seeded folder: ${mockFolderUri.toString()}`);
588
}
589
590
protected override createWorkbench(domElement: HTMLElement, serviceCollection: ServiceCollection, logService: ILogService): IBrowserMainWorkbench {
591
// Register mock-fs:// provider so all services can resolve workspace files
592
registerMockFileSystemProvider(serviceCollection);
593
594
this.preseedFolder(serviceCollection.get(IStorageService) as IStorageService);
595
596
return new SessionsWorkbench(domElement, undefined, serviceCollection, logService);
597
}
598
}
599
600