Path: blob/main/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { DisposableStore } from '../../../../../base/common/lifecycle.js';7import { URI } from '../../../../../base/common/uri.js';8import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';9import { mock } from '../../../../../base/test/common/mock.js';10import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';11import { IFileContent, IFileService } from '../../../../../platform/files/common/files.js';12import { InMemoryStorageService, IStorageService } from '../../../../../platform/storage/common/storage.js';13import { IJSONEditingService, IJSONValue } from '../../../../../workbench/services/configuration/common/jsonEditing.js';14import { IPreferencesService } from '../../../../../workbench/services/preferences/common/preferences.js';15import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js';16import { INonSessionTaskEntry, ISessionsConfigurationService, SessionsConfigurationService, ITaskEntry } from '../../browser/sessionsConfigurationService.js';17import { VSBuffer } from '../../../../../base/common/buffer.js';18import { observableValue } from '../../../../../base/common/observable.js';19import { Task } from '../../../../../workbench/contrib/tasks/common/tasks.js';20import { ITaskService } from '../../../../../workbench/contrib/tasks/common/taskService.js';21import { IChat, ISession, SessionStatus } from '../../../../services/sessions/common/session.js';22import { Codicon } from '../../../../../base/common/codicons.js';23import { IActiveSession, ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js';2425function makeSession(opts: { repository?: URI; worktree?: URI } = {}): ISession {26const workspace = opts.repository ? {27label: 'test',28icon: Codicon.folder,29repositories: [{30uri: opts.repository,31workingDirectory: opts.worktree,32detail: undefined,33baseBranchName: undefined,34}],35requiresWorkspaceTrust: false,36} : undefined;37const chat: IChat = {38resource: URI.parse('file:///session'),39createdAt: new Date(),40title: observableValue('title', 'session'),41updatedAt: observableValue('updatedAt', new Date()),42status: observableValue('status', SessionStatus.Untitled),43changes: observableValue('changes', []),44modelId: observableValue('modelId', undefined),45mode: observableValue('mode', undefined),46isArchived: observableValue('isArchived', false),47isRead: observableValue('isRead', true),48lastTurnEnd: observableValue('lastTurnEnd', undefined),49description: observableValue('description', undefined),50};51const session: ISession = {52sessionId: 'test:session',53resource: chat.resource,54providerId: 'test',55sessionType: 'background',56icon: Codicon.copilot,57createdAt: chat.createdAt,58workspace: observableValue('workspace', workspace),59title: chat.title,60updatedAt: chat.updatedAt,61status: chat.status,62changes: chat.changes,63modelId: chat.modelId,64mode: chat.mode,65loading: observableValue('loading', false),66isArchived: chat.isArchived,67isRead: chat.isRead,68lastTurnEnd: chat.lastTurnEnd,69description: chat.description,70gitHubInfo: observableValue('gitHubInfo', undefined),71chats: observableValue('chats', [chat]),72mainChat: chat,73capabilities: { supportsMultipleChats: false },74};75return session;76}7778function makeTask(label: string, command?: string, inAgents?: boolean): ITaskEntry {79return { label, type: 'shell', command: command ?? label, inAgents };80}8182function makeNpmTask(label: string, script: string, inAgents?: boolean): ITaskEntry {83return { label, type: 'npm', script, inAgents };84}8586function makeUnsupportedTask(label: string, inAgents?: boolean): ITaskEntry {87return { label, type: 'gulp', command: label, inAgents };88}8990function tasksJsonContent(tasks: ITaskEntry[]): string {91return JSON.stringify({ version: '2.0.0', tasks });92}9394suite('SessionsConfigurationService', () => {9596const store = new DisposableStore();97let service: ISessionsConfigurationService;98let fileContents: Map<string, string>;99let jsonEdits: { uri: URI; values: IJSONValue[] }[];100let ranTasks: { label: string }[];101let storageService: InMemoryStorageService;102let readFileCalls: URI[];103let activeSessionObs: ReturnType<typeof observableValue<IActiveSession | undefined>>;104let tasksByLabel: Map<string, Task>;105let workspaceFoldersByUri: Map<string, IWorkspaceFolder>;106107const userSettingsUri = URI.parse('file:///user/settings.json');108const repoUri = URI.parse('file:///repo');109const worktreeUri = URI.parse('file:///worktree');110111setup(() => {112fileContents = new Map();113jsonEdits = [];114ranTasks = [];115readFileCalls = [];116tasksByLabel = new Map();117workspaceFoldersByUri = new Map();118119const instantiationService = store.add(new TestInstantiationService());120activeSessionObs = observableValue('activeSession', undefined);121122instantiationService.stub(IFileService, new class extends mock<IFileService>() {123override async readFile(resource: URI) {124readFileCalls.push(resource);125const content = fileContents.get(resource.toString());126if (content === undefined) {127throw new Error('file not found');128}129return { value: VSBuffer.fromString(content) } as IFileContent;130}131override watch() { return { dispose() { } }; }132override onDidFilesChange: any = () => ({ dispose() { } });133});134135instantiationService.stub(IJSONEditingService, new class extends mock<IJSONEditingService>() {136override async write(resource: URI, values: IJSONValue[], _save: boolean) {137jsonEdits.push({ uri: resource, values });138}139});140141instantiationService.stub(IPreferencesService, new class extends mock<IPreferencesService>() {142override userSettingsResource = userSettingsUri;143});144145instantiationService.stub(ITaskService, new class extends mock<ITaskService>() {146override async getTask(_workspaceFolder: any, alias: string | any) {147const label = typeof alias === 'string' ? alias : '';148return tasksByLabel.get(label);149}150override async run(task: Task | undefined) {151if (task) {152ranTasks.push({ label: task._label });153}154return undefined;155}156});157158instantiationService.stub(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() {159override getWorkspaceFolder(resource: URI): IWorkspaceFolder | null {160return workspaceFoldersByUri.get(resource.toString()) ?? null;161}162});163164instantiationService.stub(ISessionsManagementService, new class extends mock<ISessionsManagementService>() {165override activeSession = activeSessionObs;166});167168storageService = store.add(new InMemoryStorageService());169instantiationService.stub(IStorageService, storageService);170171service = store.add(instantiationService.createInstance(SessionsConfigurationService));172});173174teardown(() => {175store.clear();176});177178ensureNoDisposablesAreLeakedInTestSuite();179180// --- getSessionTasks ---181182test('getSessionTasks returns tasks with inAgents: true from worktree', async () => {183const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');184fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([185makeTask('build', 'npm run build', true),186makeTask('lint', 'npm run lint', false),187makeTask('test', 'npm test', true),188makeNpmTask('watch', 'watch', true),189makeUnsupportedTask('gulp-task', true),190]));191// user tasks.json — empty192const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });193fileContents.set(userTasksUri.toString(), tasksJsonContent([]));194195const session = makeSession({ worktree: worktreeUri, repository: repoUri });196const obs = service.getSessionTasks(session);197198// Let async refresh settle199await new Promise(r => setTimeout(r, 10));200const tasks = obs.get();201202assert.deepStrictEqual(tasks.map(t => t.task.label), ['build', 'test', 'watch', 'gulp-task']);203});204205test('getSessionTasks returns empty array when no worktree', async () => {206const session = makeSession({ repository: repoUri });207const obs = service.getSessionTasks(session);208209await new Promise(r => setTimeout(r, 10));210assert.deepStrictEqual(obs.get(), []);211});212213test('getSessionTasks reads from repository when no worktree', async () => {214const repoTasksUri = URI.parse('file:///repo/.vscode/tasks.json');215fileContents.set(repoTasksUri.toString(), tasksJsonContent([216makeTask('serve', 'npm run serve', true),217makeTask('lint', 'npm run lint', false),218]));219const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });220fileContents.set(userTasksUri.toString(), tasksJsonContent([]));221222const session = makeSession({ repository: repoUri });223const obs = service.getSessionTasks(session);224225await new Promise(r => setTimeout(r, 10));226assert.deepStrictEqual(obs.get().map(t => t.task.label), ['serve']);227});228229test('getSessionTasks does not re-read files on repeated calls for the same folder', async () => {230const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');231const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });232fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([233makeTask('build', 'npm run build', true),234]));235fileContents.set(userTasksUri.toString(), tasksJsonContent([]));236237const session = makeSession({ worktree: worktreeUri, repository: repoUri });238239// Call getSessionTasks multiple times for the same session/folder240service.getSessionTasks(session);241service.getSessionTasks(session);242service.getSessionTasks(session);243244await new Promise(r => setTimeout(r, 10));245246// _refreshSessionTasks reads two files (workspace + user tasks.json).247// If refresh triggered more than once, we'd see > 2 reads.248assert.strictEqual(readFileCalls.length, 2, 'should read files only once (no duplicate refresh)');249});250251// --- getNonSessionTasks ---252253test('getNonSessionTasks returns only tasks without inAgents', async () => {254const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');255fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([256makeTask('build', 'npm run build', true),257makeTask('lint', 'npm run lint', false),258makeTask('test', 'npm test'),259makeNpmTask('watch', 'watch', false),260makeUnsupportedTask('gulp-task', false),261]));262const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });263fileContents.set(userTasksUri.toString(), tasksJsonContent([]));264265const session = makeSession({ worktree: worktreeUri, repository: repoUri });266const nonSessionTasks = await service.getNonSessionTasks(session);267268assert.deepStrictEqual(nonSessionTasks.map(t => t.task.label), ['lint', 'test', 'watch', 'gulp-task']);269});270271test('getNonSessionTasks reads from repository when no worktree', async () => {272const repoTasksUri = URI.parse('file:///repo/.vscode/tasks.json');273fileContents.set(repoTasksUri.toString(), tasksJsonContent([274makeTask('build', 'npm run build', true),275makeTask('lint', 'npm run lint', false),276]));277const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });278fileContents.set(userTasksUri.toString(), tasksJsonContent([]));279280const session = makeSession({ repository: repoUri });281const nonSessionTasks = await service.getNonSessionTasks(session);282283assert.deepStrictEqual(nonSessionTasks.map(t => t.task.label), ['lint']);284});285286test('getNonSessionTasks preserves the source target for workspace and user tasks', async () => {287const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');288const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });289fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([290makeTask('workspaceTask', 'npm run workspace'),291]));292fileContents.set(userTasksUri.toString(), tasksJsonContent([293makeTask('userTask', 'npm run user'),294]));295296const session = makeSession({ worktree: worktreeUri, repository: repoUri });297const nonSessionTasks = await service.getNonSessionTasks(session);298299assert.deepStrictEqual(nonSessionTasks, [300{ task: { label: 'workspaceTask', type: 'shell', command: 'npm run workspace' }, target: 'workspace' },301{ task: { label: 'userTask', type: 'shell', command: 'npm run user' }, target: 'user' },302] satisfies INonSessionTaskEntry[]);303});304305// --- addTaskToSessions ---306307test('addTaskToSessions writes inAgents: true to the matching task index', async () => {308const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');309fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([310makeTask('build', 'npm run build'),311makeTask('test', 'npm test'),312]));313314const session = makeSession({ worktree: worktreeUri, repository: repoUri });315const task = makeTask('test', 'npm test');316await service.addTaskToSessions(task, session, 'workspace');317318assert.strictEqual(jsonEdits.length, 1);319assert.deepStrictEqual(jsonEdits[0].values, [{ path: ['tasks', 1, 'inAgents'], value: true }]);320});321322test('addTaskToSessions does nothing when task label not found', async () => {323const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');324fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([325makeTask('build', 'npm run build'),326]));327328const session = makeSession({ worktree: worktreeUri, repository: repoUri });329await service.addTaskToSessions(makeTask('nonexistent'), session, 'workspace');330331assert.strictEqual(jsonEdits.length, 0);332});333334test('addTaskToSessions writes to repository and does not commit when no worktree', async () => {335const repoTasksUri = URI.parse('file:///repo/.vscode/tasks.json');336fileContents.set(repoTasksUri.toString(), tasksJsonContent([337makeTask('build', 'npm run build'),338makeTask('test', 'npm test'),339]));340341const session = makeSession({ repository: repoUri });342await service.addTaskToSessions(makeTask('test', 'npm test'), session, 'workspace');343344assert.strictEqual(jsonEdits.length, 1);345assert.strictEqual(jsonEdits[0].uri.toString(), repoTasksUri.toString());346assert.deepStrictEqual(jsonEdits[0].values, [{ path: ['tasks', 1, 'inAgents'], value: true }]);347});348349test('addTaskToSessions updates runOptions when provided', async () => {350const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');351fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([352makeTask('build', 'npm run build'),353]));354355const session = makeSession({ worktree: worktreeUri, repository: repoUri });356await service.addTaskToSessions(makeTask('build', 'npm run build'), session, 'workspace', { runOn: 'worktreeCreated' });357358assert.deepStrictEqual(jsonEdits[0].values, [359{ path: ['tasks', 0, 'inAgents'], value: true },360{ path: ['tasks', 0, 'runOptions'], value: { runOn: 'worktreeCreated' } },361]);362});363364test('addTaskToSessions clears runOptions when default is requested', async () => {365const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');366fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([367{ ...makeTask('build', 'npm run build'), runOptions: { runOn: 'worktreeCreated' } },368]));369370const session = makeSession({ worktree: worktreeUri, repository: repoUri });371await service.addTaskToSessions(makeTask('build', 'npm run build'), session, 'workspace', { runOn: 'default' });372373assert.deepStrictEqual(jsonEdits[0].values, [374{ path: ['tasks', 0, 'inAgents'], value: true },375{ path: ['tasks', 0, 'runOptions'], value: undefined },376]);377});378379// --- createAndAddTask ---380381test('createAndAddTask writes new task with inAgents: true', async () => {382const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');383fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([384makeTask('existing', 'echo hi'),385]));386387const session = makeSession({ worktree: worktreeUri, repository: repoUri });388await service.createAndAddTask(undefined, 'npm run dev', session, 'workspace');389390assert.strictEqual(jsonEdits.length, 1);391const edit = jsonEdits[0];392assert.strictEqual(edit.uri.toString(), worktreeTasksUri.toString());393const tasksValue = edit.values.find(v => v.path[0] === 'tasks');394assert.ok(tasksValue);395const tasks = tasksValue!.value as ITaskEntry[];396assert.strictEqual(tasks.length, 2);397assert.strictEqual(tasks[1].label, 'npm run dev');398assert.strictEqual(tasks[1].inAgents, true);399});400401test('createAndAddTask writes to repository and does not commit when no worktree', async () => {402const repoTasksUri = URI.parse('file:///repo/.vscode/tasks.json');403fileContents.set(repoTasksUri.toString(), tasksJsonContent([404makeTask('existing', 'echo hi'),405]));406407const session = makeSession({ repository: repoUri });408await service.createAndAddTask(undefined, 'npm run dev', session, 'workspace');409410assert.strictEqual(jsonEdits.length, 1);411assert.strictEqual(jsonEdits[0].uri.toString(), repoTasksUri.toString());412const tasksValue = jsonEdits[0].values.find(v => v.path[0] === 'tasks');413assert.ok(tasksValue);414const tasks = tasksValue!.value as ITaskEntry[];415assert.strictEqual(tasks.length, 2);416assert.strictEqual(tasks[1].label, 'npm run dev');417assert.strictEqual(tasks[1].inAgents, true);418});419420test('createAndAddTask writes worktreeCreated run option when requested', async () => {421const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');422fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([]));423424const session = makeSession({ worktree: worktreeUri, repository: repoUri });425await service.createAndAddTask(undefined, 'npm run dev', session, 'workspace', { runOn: 'worktreeCreated' });426427assert.strictEqual(jsonEdits.length, 1);428const tasksValue = jsonEdits[0].values.find(v => v.path[0] === 'tasks');429assert.ok(tasksValue);430const tasks = tasksValue!.value as ITaskEntry[];431assert.deepStrictEqual(tasks[0].runOptions, { runOn: 'worktreeCreated' });432});433434test('createAndAddTask writes a custom label when provided', async () => {435const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');436fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([]));437438const session = makeSession({ worktree: worktreeUri, repository: repoUri });439await service.createAndAddTask('Start Dev Server', 'npm run dev', session, 'workspace');440441assert.strictEqual(jsonEdits.length, 1);442const tasksValue = jsonEdits[0].values.find(v => v.path[0] === 'tasks');443assert.ok(tasksValue);444const tasks = tasksValue!.value as ITaskEntry[];445assert.strictEqual(tasks[0].label, 'Start Dev Server');446assert.strictEqual(tasks[0].command, 'npm run dev');447});448449// --- removeTask ---450451test('removeTask deletes the matching task entry', async () => {452const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');453fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([454makeTask('build', 'npm run build', true),455makeTask('test', 'npm test', true),456makeTask('lint', 'npm run lint'),457]));458459const session = makeSession({ worktree: worktreeUri, repository: repoUri });460await service.removeTask('test', session, 'workspace');461462assert.strictEqual(jsonEdits.length, 1);463assert.deepStrictEqual(jsonEdits[0].values, [{464path: ['tasks'],465value: [466makeTask('build', 'npm run build', true),467{ label: 'lint', type: 'shell', command: 'npm run lint' },468],469}]);470});471472// --- updateTask ---473474test('updateTask replaces an existing task in place', async () => {475const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');476fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([477makeTask('build', 'npm run build', true),478makeTask('test', 'npm test', true),479]));480481const session = makeSession({ worktree: worktreeUri, repository: repoUri });482await service.updateTask('test', {483label: 'Test Changed',484type: 'shell',485command: 'pnpm test',486inAgents: true,487runOptions: { runOn: 'worktreeCreated' }488}, session, 'workspace', 'workspace');489490assert.strictEqual(jsonEdits.length, 1);491assert.deepStrictEqual(jsonEdits[0].values, [{492path: ['tasks'],493value: [494makeTask('build', 'npm run build', true),495{496label: 'Test Changed',497type: 'shell',498command: 'pnpm test',499inAgents: true,500runOptions: { runOn: 'worktreeCreated' }501}502]503}]);504});505506test('updateTask moves a task between workspace and user storage', async () => {507const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');508const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' });509fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([510makeTask('build', 'npm run build', true),511]));512fileContents.set(userTasksUri.toString(), tasksJsonContent([513makeTask('userExisting', 'npm run user', true),514]));515516const session = makeSession({ worktree: worktreeUri, repository: repoUri });517await service.updateTask('build', {518label: 'Build Changed',519type: 'shell',520command: 'pnpm build',521inAgents: true,522}, session, 'workspace', 'user');523524assert.strictEqual(jsonEdits.length, 2);525assert.deepStrictEqual(jsonEdits[0], {526uri: worktreeTasksUri,527values: [{528path: ['tasks'],529value: []530}]531});532assert.deepStrictEqual(jsonEdits[1], {533uri: userTasksUri,534values: [535{ path: ['version'], value: '2.0.0' },536{537path: ['tasks'],538value: [539makeTask('userExisting', 'npm run user', true),540{541label: 'Build Changed',542type: 'shell',543command: 'pnpm build',544inAgents: true,545}546]547}548]549});550});551552// --- pinned task ---553554test('getPinnedTaskLabel returns undefined when no task is pinned', () => {555const obs = service.getPinnedTaskLabel(repoUri);556assert.strictEqual(obs.get(), undefined);557});558559test('setPinnedTaskLabel stores and clears the pinned task label', () => {560const obs = service.getPinnedTaskLabel(repoUri);561562service.setPinnedTaskLabel(repoUri, 'build');563assert.strictEqual(obs.get(), 'build');564565service.setPinnedTaskLabel(repoUri, undefined);566assert.strictEqual(obs.get(), undefined);567});568569test('updateTask keeps the pinned task in sync when the label changes', async () => {570const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');571fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([572makeTask('build', 'npm run build', true),573]));574service.setPinnedTaskLabel(repoUri, 'build');575576const session = makeSession({ worktree: worktreeUri, repository: repoUri });577await service.updateTask('build', {578label: 'build:watch',579type: 'shell',580command: 'npm run watch',581inAgents: true,582}, session, 'workspace', 'workspace');583584assert.strictEqual(service.getPinnedTaskLabel(repoUri).get(), 'build:watch');585});586587test('removeTask clears the pinned task when deleting the pinned entry', async () => {588const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json');589fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([590makeTask('build', 'npm run build', true),591]));592service.setPinnedTaskLabel(repoUri, 'build');593594const session = makeSession({ worktree: worktreeUri, repository: repoUri });595await service.removeTask('build', session, 'workspace');596597assert.strictEqual(service.getPinnedTaskLabel(repoUri).get(), undefined);598});599600// --- runTask ---601602function registerMockTask(label: string, folder: URI): void {603tasksByLabel.set(label, { _label: label } as unknown as Task);604workspaceFoldersByUri.set(folder.toString(), { uri: folder, name: 'folder', index: 0, toResource: () => folder } as IWorkspaceFolder);605}606607test('runTask looks up task by label and runs it via the task service', async () => {608registerMockTask('build', worktreeUri);609const session = makeSession({ worktree: worktreeUri, repository: repoUri });610611await service.runTask(makeTask('build', 'npm run build'), session);612613assert.strictEqual(ranTasks.length, 1);614assert.strictEqual(ranTasks[0].label, 'build');615});616617test('runTask does nothing when no cwd available', async () => {618const session = makeSession({ repository: undefined, worktree: undefined });619await service.runTask(makeTask('build', 'npm run build'), session);620621assert.strictEqual(ranTasks.length, 0);622});623624test('runTask does nothing when workspace folder not found', async () => {625// No workspace folder registered for worktreeUri626const session = makeSession({ worktree: worktreeUri, repository: repoUri });627await service.runTask(makeTask('build', 'npm run build'), session);628629assert.strictEqual(ranTasks.length, 0);630});631632test('runTask does nothing when task not found by label', async () => {633workspaceFoldersByUri.set(worktreeUri.toString(), { uri: worktreeUri, name: 'folder', index: 0, toResource: () => worktreeUri } as IWorkspaceFolder);634// No task registered for 'nonexistent'635const session = makeSession({ worktree: worktreeUri, repository: repoUri });636await service.runTask(makeTask('nonexistent', 'echo hi'), session);637638assert.strictEqual(ranTasks.length, 0);639});640641test('runTask uses repository as cwd when worktree is not available', async () => {642registerMockTask('build', repoUri);643const session = makeSession({ repository: repoUri });644645await service.runTask(makeTask('build', 'npm run build'), session);646647assert.strictEqual(ranTasks.length, 1);648assert.strictEqual(ranTasks[0].label, 'build');649});650});651652653