Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.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 assert from 'assert';
7
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
10
import { mock } from '../../../../../base/test/common/mock.js';
11
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
12
import { IFileContent, IFileService } from '../../../../../platform/files/common/files.js';
13
import { InMemoryStorageService, IStorageService } from '../../../../../platform/storage/common/storage.js';
14
import { IJSONEditingService, IJSONValue } from '../../../../../workbench/services/configuration/common/jsonEditing.js';
15
import { IPreferencesService } from '../../../../../workbench/services/preferences/common/preferences.js';
16
import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js';
17
import { INonSessionTaskEntry, ISessionsConfigurationService, SessionsConfigurationService, ITaskEntry } from '../../browser/sessionsConfigurationService.js';
18
import { VSBuffer } from '../../../../../base/common/buffer.js';
19
import { observableValue } from '../../../../../base/common/observable.js';
20
import { Task } from '../../../../../workbench/contrib/tasks/common/tasks.js';
21
import { ITaskService } from '../../../../../workbench/contrib/tasks/common/taskService.js';
22
import { IChat, ISession, SessionStatus } from '../../../../services/sessions/common/session.js';
23
import { Codicon } from '../../../../../base/common/codicons.js';
24
import { IActiveSession, ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js';
25
26
function makeSession(opts: { repository?: URI; worktree?: URI } = {}): ISession {
27
const workspace = opts.repository ? {
28
label: 'test',
29
icon: Codicon.folder,
30
repositories: [{
31
uri: opts.repository,
32
workingDirectory: opts.worktree,
33
detail: undefined,
34
baseBranchName: undefined,
35
}],
36
requiresWorkspaceTrust: false,
37
} : undefined;
38
const chat: IChat = {
39
resource: URI.parse('file:///session'),
40
createdAt: new Date(),
41
title: observableValue('title', 'session'),
42
updatedAt: observableValue('updatedAt', new Date()),
43
status: observableValue('status', SessionStatus.Untitled),
44
changes: observableValue('changes', []),
45
modelId: observableValue('modelId', undefined),
46
mode: observableValue('mode', undefined),
47
isArchived: observableValue('isArchived', false),
48
isRead: observableValue('isRead', true),
49
lastTurnEnd: observableValue('lastTurnEnd', undefined),
50
description: observableValue('description', undefined),
51
};
52
const session: ISession = {
53
sessionId: 'test:session',
54
resource: chat.resource,
55
providerId: 'test',
56
sessionType: 'background',
57
icon: Codicon.copilot,
58
createdAt: chat.createdAt,
59
workspace: observableValue('workspace', workspace),
60
title: chat.title,
61
updatedAt: chat.updatedAt,
62
status: chat.status,
63
changes: chat.changes,
64
modelId: chat.modelId,
65
mode: chat.mode,
66
loading: observableValue('loading', false),
67
isArchived: chat.isArchived,
68
isRead: chat.isRead,
69
lastTurnEnd: chat.lastTurnEnd,
70
description: chat.description,
71
gitHubInfo: observableValue('gitHubInfo', undefined),
72
chats: observableValue('chats', [chat]),
73
mainChat: chat,
74
capabilities: { supportsMultipleChats: false },
75
};
76
return session;
77
}
78
79
function makeTask(label: string, command?: string, inAgents?: boolean): ITaskEntry {
80
return { label, type: 'shell', command: command ?? label, inAgents };
81
}
82
83
function makeNpmTask(label: string, script: string, inAgents?: boolean): ITaskEntry {
84
return { label, type: 'npm', script, inAgents };
85
}
86
87
function makeUnsupportedTask(label: string, inAgents?: boolean): ITaskEntry {
88
return { label, type: 'gulp', command: label, inAgents };
89
}
90
91
function tasksJsonContent(tasks: ITaskEntry[]): string {
92
return JSON.stringify({ version: '2.0.0', tasks });
93
}
94
95
suite('SessionsConfigurationService', () => {
96
97
const store = new DisposableStore();
98
let service: ISessionsConfigurationService;
99
let fileContents: Map<string, string>;
100
let jsonEdits: { uri: URI; values: IJSONValue[] }[];
101
let ranTasks: { label: string }[];
102
let storageService: InMemoryStorageService;
103
let readFileCalls: URI[];
104
let activeSessionObs: ReturnType<typeof observableValue<IActiveSession | undefined>>;
105
let tasksByLabel: Map<string, Task>;
106
let workspaceFoldersByUri: Map<string, IWorkspaceFolder>;
107
108
const userSettingsUri = URI.parse('file:///user/settings.json');
109
const repoUri = URI.parse('file:///repo');
110
const worktreeUri = URI.parse('file:///worktree');
111
112
setup(() => {
113
fileContents = new Map();
114
jsonEdits = [];
115
ranTasks = [];
116
readFileCalls = [];
117
tasksByLabel = new Map();
118
workspaceFoldersByUri = new Map();
119
120
const instantiationService = store.add(new TestInstantiationService());
121
activeSessionObs = observableValue('activeSession', undefined);
122
123
instantiationService.stub(IFileService, new class extends mock<IFileService>() {
124
override async readFile(resource: URI) {
125
readFileCalls.push(resource);
126
const content = fileContents.get(resource.toString());
127
if (content === undefined) {
128
throw new Error('file not found');
129
}
130
return { value: VSBuffer.fromString(content) } as IFileContent;
131
}
132
override watch() { return { dispose() { } }; }
133
override onDidFilesChange: any = () => ({ dispose() { } });
134
});
135
136
instantiationService.stub(IJSONEditingService, new class extends mock<IJSONEditingService>() {
137
override async write(resource: URI, values: IJSONValue[], _save: boolean) {
138
jsonEdits.push({ uri: resource, values });
139
}
140
});
141
142
instantiationService.stub(IPreferencesService, new class extends mock<IPreferencesService>() {
143
override userSettingsResource = userSettingsUri;
144
});
145
146
instantiationService.stub(ITaskService, new class extends mock<ITaskService>() {
147
override async getTask(_workspaceFolder: any, alias: string | any) {
148
const label = typeof alias === 'string' ? alias : '';
149
return tasksByLabel.get(label);
150
}
151
override async run(task: Task | undefined) {
152
if (task) {
153
ranTasks.push({ label: task._label });
154
}
155
return undefined;
156
}
157
});
158
159
instantiationService.stub(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() {
160
override getWorkspaceFolder(resource: URI): IWorkspaceFolder | null {
161
return workspaceFoldersByUri.get(resource.toString()) ?? null;
162
}
163
});
164
165
instantiationService.stub(ISessionsManagementService, new class extends mock<ISessionsManagementService>() {
166
override activeSession = activeSessionObs;
167
});
168
169
storageService = store.add(new InMemoryStorageService());
170
instantiationService.stub(IStorageService, storageService);
171
172
service = store.add(instantiationService.createInstance(SessionsConfigurationService));
173
});
174
175
teardown(() => {
176
store.clear();
177
});
178
179
ensureNoDisposablesAreLeakedInTestSuite();
180
181
// --- getSessionTasks ---
182
183
test('getSessionTasks returns tasks with inAgents: true from worktree', async () => {
184
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
185
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
186
makeTask('build', 'npm run build', true),
187
makeTask('lint', 'npm run lint', false),
188
makeTask('test', 'npm test', true),
189
makeNpmTask('watch', 'watch', true),
190
makeUnsupportedTask('gulp-task', true),
191
]));
192
// user tasks.json — empty
193
const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });
194
fileContents.set(userTasksUri.toString(), tasksJsonContent([]));
195
196
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
197
const obs = service.getSessionTasks(session);
198
199
// Let async refresh settle
200
await new Promise(r => setTimeout(r, 10));
201
const tasks = obs.get();
202
203
assert.deepStrictEqual(tasks.map(t => t.task.label), ['build', 'test', 'watch', 'gulp-task']);
204
});
205
206
test('getSessionTasks returns empty array when no worktree', async () => {
207
const session = makeSession({ repository: repoUri });
208
const obs = service.getSessionTasks(session);
209
210
await new Promise(r => setTimeout(r, 10));
211
assert.deepStrictEqual(obs.get(), []);
212
});
213
214
test('getSessionTasks reads from repository when no worktree', async () => {
215
const repoTasksUri = URI.parse('file:///repo/.vscode/tasks.json');
216
fileContents.set(repoTasksUri.toString(), tasksJsonContent([
217
makeTask('serve', 'npm run serve', true),
218
makeTask('lint', 'npm run lint', false),
219
]));
220
const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });
221
fileContents.set(userTasksUri.toString(), tasksJsonContent([]));
222
223
const session = makeSession({ repository: repoUri });
224
const obs = service.getSessionTasks(session);
225
226
await new Promise(r => setTimeout(r, 10));
227
assert.deepStrictEqual(obs.get().map(t => t.task.label), ['serve']);
228
});
229
230
test('getSessionTasks does not re-read files on repeated calls for the same folder', async () => {
231
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
232
const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });
233
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
234
makeTask('build', 'npm run build', true),
235
]));
236
fileContents.set(userTasksUri.toString(), tasksJsonContent([]));
237
238
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
239
240
// Call getSessionTasks multiple times for the same session/folder
241
service.getSessionTasks(session);
242
service.getSessionTasks(session);
243
service.getSessionTasks(session);
244
245
await new Promise(r => setTimeout(r, 10));
246
247
// _refreshSessionTasks reads two files (workspace + user tasks.json).
248
// If refresh triggered more than once, we'd see > 2 reads.
249
assert.strictEqual(readFileCalls.length, 2, 'should read files only once (no duplicate refresh)');
250
});
251
252
// --- getNonSessionTasks ---
253
254
test('getNonSessionTasks returns only tasks without inAgents', async () => {
255
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
256
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
257
makeTask('build', 'npm run build', true),
258
makeTask('lint', 'npm run lint', false),
259
makeTask('test', 'npm test'),
260
makeNpmTask('watch', 'watch', false),
261
makeUnsupportedTask('gulp-task', false),
262
]));
263
const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });
264
fileContents.set(userTasksUri.toString(), tasksJsonContent([]));
265
266
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
267
const nonSessionTasks = await service.getNonSessionTasks(session);
268
269
assert.deepStrictEqual(nonSessionTasks.map(t => t.task.label), ['lint', 'test', 'watch', 'gulp-task']);
270
});
271
272
test('getNonSessionTasks reads from repository when no worktree', async () => {
273
const repoTasksUri = URI.parse('file:///repo/.vscode/tasks.json');
274
fileContents.set(repoTasksUri.toString(), tasksJsonContent([
275
makeTask('build', 'npm run build', true),
276
makeTask('lint', 'npm run lint', false),
277
]));
278
const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });
279
fileContents.set(userTasksUri.toString(), tasksJsonContent([]));
280
281
const session = makeSession({ repository: repoUri });
282
const nonSessionTasks = await service.getNonSessionTasks(session);
283
284
assert.deepStrictEqual(nonSessionTasks.map(t => t.task.label), ['lint']);
285
});
286
287
test('getNonSessionTasks preserves the source target for workspace and user tasks', async () => {
288
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
289
const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });
290
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
291
makeTask('workspaceTask', 'npm run workspace'),
292
]));
293
fileContents.set(userTasksUri.toString(), tasksJsonContent([
294
makeTask('userTask', 'npm run user'),
295
]));
296
297
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
298
const nonSessionTasks = await service.getNonSessionTasks(session);
299
300
assert.deepStrictEqual(nonSessionTasks, [
301
{ task: { label: 'workspaceTask', type: 'shell', command: 'npm run workspace' }, target: 'workspace' },
302
{ task: { label: 'userTask', type: 'shell', command: 'npm run user' }, target: 'user' },
303
] satisfies INonSessionTaskEntry[]);
304
});
305
306
// --- addTaskToSessions ---
307
308
test('addTaskToSessions writes inAgents: true to the matching task index', async () => {
309
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
310
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
311
makeTask('build', 'npm run build'),
312
makeTask('test', 'npm test'),
313
]));
314
315
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
316
const task = makeTask('test', 'npm test');
317
await service.addTaskToSessions(task, session, 'workspace');
318
319
assert.strictEqual(jsonEdits.length, 1);
320
assert.deepStrictEqual(jsonEdits[0].values, [{ path: ['tasks', 1, 'inAgents'], value: true }]);
321
});
322
323
test('addTaskToSessions does nothing when task label not found', async () => {
324
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
325
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
326
makeTask('build', 'npm run build'),
327
]));
328
329
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
330
await service.addTaskToSessions(makeTask('nonexistent'), session, 'workspace');
331
332
assert.strictEqual(jsonEdits.length, 0);
333
});
334
335
test('addTaskToSessions writes to repository and does not commit when no worktree', async () => {
336
const repoTasksUri = URI.parse('file:///repo/.vscode/tasks.json');
337
fileContents.set(repoTasksUri.toString(), tasksJsonContent([
338
makeTask('build', 'npm run build'),
339
makeTask('test', 'npm test'),
340
]));
341
342
const session = makeSession({ repository: repoUri });
343
await service.addTaskToSessions(makeTask('test', 'npm test'), session, 'workspace');
344
345
assert.strictEqual(jsonEdits.length, 1);
346
assert.strictEqual(jsonEdits[0].uri.toString(), repoTasksUri.toString());
347
assert.deepStrictEqual(jsonEdits[0].values, [{ path: ['tasks', 1, 'inAgents'], value: true }]);
348
});
349
350
test('addTaskToSessions updates runOptions when provided', async () => {
351
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
352
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
353
makeTask('build', 'npm run build'),
354
]));
355
356
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
357
await service.addTaskToSessions(makeTask('build', 'npm run build'), session, 'workspace', { runOn: 'worktreeCreated' });
358
359
assert.deepStrictEqual(jsonEdits[0].values, [
360
{ path: ['tasks', 0, 'inAgents'], value: true },
361
{ path: ['tasks', 0, 'runOptions'], value: { runOn: 'worktreeCreated' } },
362
]);
363
});
364
365
test('addTaskToSessions clears runOptions when default is requested', async () => {
366
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
367
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
368
{ ...makeTask('build', 'npm run build'), runOptions: { runOn: 'worktreeCreated' } },
369
]));
370
371
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
372
await service.addTaskToSessions(makeTask('build', 'npm run build'), session, 'workspace', { runOn: 'default' });
373
374
assert.deepStrictEqual(jsonEdits[0].values, [
375
{ path: ['tasks', 0, 'inAgents'], value: true },
376
{ path: ['tasks', 0, 'runOptions'], value: undefined },
377
]);
378
});
379
380
// --- createAndAddTask ---
381
382
test('createAndAddTask writes new task with inAgents: true', async () => {
383
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
384
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
385
makeTask('existing', 'echo hi'),
386
]));
387
388
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
389
await service.createAndAddTask(undefined, 'npm run dev', session, 'workspace');
390
391
assert.strictEqual(jsonEdits.length, 1);
392
const edit = jsonEdits[0];
393
assert.strictEqual(edit.uri.toString(), worktreeTasksUri.toString());
394
const tasksValue = edit.values.find(v => v.path[0] === 'tasks');
395
assert.ok(tasksValue);
396
const tasks = tasksValue!.value as ITaskEntry[];
397
assert.strictEqual(tasks.length, 2);
398
assert.strictEqual(tasks[1].label, 'npm run dev');
399
assert.strictEqual(tasks[1].inAgents, true);
400
});
401
402
test('createAndAddTask writes to repository and does not commit when no worktree', async () => {
403
const repoTasksUri = URI.parse('file:///repo/.vscode/tasks.json');
404
fileContents.set(repoTasksUri.toString(), tasksJsonContent([
405
makeTask('existing', 'echo hi'),
406
]));
407
408
const session = makeSession({ repository: repoUri });
409
await service.createAndAddTask(undefined, 'npm run dev', session, 'workspace');
410
411
assert.strictEqual(jsonEdits.length, 1);
412
assert.strictEqual(jsonEdits[0].uri.toString(), repoTasksUri.toString());
413
const tasksValue = jsonEdits[0].values.find(v => v.path[0] === 'tasks');
414
assert.ok(tasksValue);
415
const tasks = tasksValue!.value as ITaskEntry[];
416
assert.strictEqual(tasks.length, 2);
417
assert.strictEqual(tasks[1].label, 'npm run dev');
418
assert.strictEqual(tasks[1].inAgents, true);
419
});
420
421
test('createAndAddTask writes worktreeCreated run option when requested', async () => {
422
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
423
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([]));
424
425
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
426
await service.createAndAddTask(undefined, 'npm run dev', session, 'workspace', { runOn: 'worktreeCreated' });
427
428
assert.strictEqual(jsonEdits.length, 1);
429
const tasksValue = jsonEdits[0].values.find(v => v.path[0] === 'tasks');
430
assert.ok(tasksValue);
431
const tasks = tasksValue!.value as ITaskEntry[];
432
assert.deepStrictEqual(tasks[0].runOptions, { runOn: 'worktreeCreated' });
433
});
434
435
test('createAndAddTask writes a custom label when provided', async () => {
436
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
437
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([]));
438
439
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
440
await service.createAndAddTask('Start Dev Server', 'npm run dev', session, 'workspace');
441
442
assert.strictEqual(jsonEdits.length, 1);
443
const tasksValue = jsonEdits[0].values.find(v => v.path[0] === 'tasks');
444
assert.ok(tasksValue);
445
const tasks = tasksValue!.value as ITaskEntry[];
446
assert.strictEqual(tasks[0].label, 'Start Dev Server');
447
assert.strictEqual(tasks[0].command, 'npm run dev');
448
});
449
450
// --- removeTask ---
451
452
test('removeTask deletes the matching task entry', async () => {
453
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
454
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
455
makeTask('build', 'npm run build', true),
456
makeTask('test', 'npm test', true),
457
makeTask('lint', 'npm run lint'),
458
]));
459
460
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
461
await service.removeTask('test', session, 'workspace');
462
463
assert.strictEqual(jsonEdits.length, 1);
464
assert.deepStrictEqual(jsonEdits[0].values, [{
465
path: ['tasks'],
466
value: [
467
makeTask('build', 'npm run build', true),
468
{ label: 'lint', type: 'shell', command: 'npm run lint' },
469
],
470
}]);
471
});
472
473
// --- updateTask ---
474
475
test('updateTask replaces an existing task in place', async () => {
476
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
477
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
478
makeTask('build', 'npm run build', true),
479
makeTask('test', 'npm test', true),
480
]));
481
482
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
483
await service.updateTask('test', {
484
label: 'Test Changed',
485
type: 'shell',
486
command: 'pnpm test',
487
inAgents: true,
488
runOptions: { runOn: 'worktreeCreated' }
489
}, session, 'workspace', 'workspace');
490
491
assert.strictEqual(jsonEdits.length, 1);
492
assert.deepStrictEqual(jsonEdits[0].values, [{
493
path: ['tasks'],
494
value: [
495
makeTask('build', 'npm run build', true),
496
{
497
label: 'Test Changed',
498
type: 'shell',
499
command: 'pnpm test',
500
inAgents: true,
501
runOptions: { runOn: 'worktreeCreated' }
502
}
503
]
504
}]);
505
});
506
507
test('updateTask moves a task between workspace and user storage', async () => {
508
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
509
const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });
510
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
511
makeTask('build', 'npm run build', true),
512
]));
513
fileContents.set(userTasksUri.toString(), tasksJsonContent([
514
makeTask('userExisting', 'npm run user', true),
515
]));
516
517
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
518
await service.updateTask('build', {
519
label: 'Build Changed',
520
type: 'shell',
521
command: 'pnpm build',
522
inAgents: true,
523
}, session, 'workspace', 'user');
524
525
assert.strictEqual(jsonEdits.length, 2);
526
assert.deepStrictEqual(jsonEdits[0], {
527
uri: worktreeTasksUri,
528
values: [{
529
path: ['tasks'],
530
value: []
531
}]
532
});
533
assert.deepStrictEqual(jsonEdits[1], {
534
uri: userTasksUri,
535
values: [
536
{ path: ['version'], value: '2.0.0' },
537
{
538
path: ['tasks'],
539
value: [
540
makeTask('userExisting', 'npm run user', true),
541
{
542
label: 'Build Changed',
543
type: 'shell',
544
command: 'pnpm build',
545
inAgents: true,
546
}
547
]
548
}
549
]
550
});
551
});
552
553
// --- pinned task ---
554
555
test('getPinnedTaskLabel returns undefined when no task is pinned', () => {
556
const obs = service.getPinnedTaskLabel(repoUri);
557
assert.strictEqual(obs.get(), undefined);
558
});
559
560
test('setPinnedTaskLabel stores and clears the pinned task label', () => {
561
const obs = service.getPinnedTaskLabel(repoUri);
562
563
service.setPinnedTaskLabel(repoUri, 'build');
564
assert.strictEqual(obs.get(), 'build');
565
566
service.setPinnedTaskLabel(repoUri, undefined);
567
assert.strictEqual(obs.get(), undefined);
568
});
569
570
test('updateTask keeps the pinned task in sync when the label changes', async () => {
571
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
572
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
573
makeTask('build', 'npm run build', true),
574
]));
575
service.setPinnedTaskLabel(repoUri, 'build');
576
577
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
578
await service.updateTask('build', {
579
label: 'build:watch',
580
type: 'shell',
581
command: 'npm run watch',
582
inAgents: true,
583
}, session, 'workspace', 'workspace');
584
585
assert.strictEqual(service.getPinnedTaskLabel(repoUri).get(), 'build:watch');
586
});
587
588
test('removeTask clears the pinned task when deleting the pinned entry', async () => {
589
const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');
590
fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([
591
makeTask('build', 'npm run build', true),
592
]));
593
service.setPinnedTaskLabel(repoUri, 'build');
594
595
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
596
await service.removeTask('build', session, 'workspace');
597
598
assert.strictEqual(service.getPinnedTaskLabel(repoUri).get(), undefined);
599
});
600
601
// --- runTask ---
602
603
function registerMockTask(label: string, folder: URI): void {
604
tasksByLabel.set(label, { _label: label } as unknown as Task);
605
workspaceFoldersByUri.set(folder.toString(), { uri: folder, name: 'folder', index: 0, toResource: () => folder } as IWorkspaceFolder);
606
}
607
608
test('runTask looks up task by label and runs it via the task service', async () => {
609
registerMockTask('build', worktreeUri);
610
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
611
612
await service.runTask(makeTask('build', 'npm run build'), session);
613
614
assert.strictEqual(ranTasks.length, 1);
615
assert.strictEqual(ranTasks[0].label, 'build');
616
});
617
618
test('runTask does nothing when no cwd available', async () => {
619
const session = makeSession({ repository: undefined, worktree: undefined });
620
await service.runTask(makeTask('build', 'npm run build'), session);
621
622
assert.strictEqual(ranTasks.length, 0);
623
});
624
625
test('runTask does nothing when workspace folder not found', async () => {
626
// No workspace folder registered for worktreeUri
627
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
628
await service.runTask(makeTask('build', 'npm run build'), session);
629
630
assert.strictEqual(ranTasks.length, 0);
631
});
632
633
test('runTask does nothing when task not found by label', async () => {
634
workspaceFoldersByUri.set(worktreeUri.toString(), { uri: worktreeUri, name: 'folder', index: 0, toResource: () => worktreeUri } as IWorkspaceFolder);
635
// No task registered for 'nonexistent'
636
const session = makeSession({ worktree: worktreeUri, repository: repoUri });
637
await service.runTask(makeTask('nonexistent', 'echo hi'), session);
638
639
assert.strictEqual(ranTasks.length, 0);
640
});
641
642
test('runTask uses repository as cwd when worktree is not available', async () => {
643
registerMockTask('build', repoUri);
644
const session = makeSession({ repository: repoUri });
645
646
await service.runTask(makeTask('build', 'npm run build'), session);
647
648
assert.strictEqual(ranTasks.length, 1);
649
assert.strictEqual(ranTasks[0].label, 'build');
650
});
651
});
652
653