Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.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 { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { Emitter, Event } from '../../../../../base/common/event.js';
9
import { mock } from '../../../../../base/test/common/mock.js';
10
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
11
import { type IAgentConnection } from '../../../../../platform/agentHost/common/agentService.js';
12
import { ActionType, type ActionEnvelope, type INotification, type StateAction } from '../../../../../platform/agentHost/common/state/sessionActions.js';
13
import { CustomizationStatus, type AgentInfo, type CustomizationRef, type RootState, type SessionCustomization } from '../../../../../platform/agentHost/common/state/protocol/state.js';
14
import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
15
import { VSBuffer } from '../../../../../base/common/buffer.js';
16
import { IFileService, type IFileContent, type IFileStat, type IFileStatResult } from '../../../../../platform/files/common/files.js';
17
import { PromptsType } from '../../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
18
import { NullLogService } from '../../../../../platform/log/common/log.js';
19
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
20
import { URI } from '../../../../../base/common/uri.js';
21
import { IAICustomizationWorkspaceService } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
22
import { SYNCED_CUSTOMIZATION_SCHEME } from '../../../../../workbench/services/agentHost/common/agentHostFileSystemService.js';
23
import { RemoteAgentCustomizationItemProvider, RemoteAgentPluginController } from '../../browser/remoteAgentHostCustomizationHarness.js';
24
25
class MockAgentConnection extends mock<IAgentConnection>() {
26
declare readonly _serviceBrand: undefined;
27
28
private readonly _onDidAction = new Emitter<ActionEnvelope>();
29
override readonly onDidAction = this._onDidAction.event;
30
override readonly onDidNotification = Event.None as Event<INotification>;
31
override readonly clientId = 'test-client';
32
33
private _rootStateValue: RootState = { agents: [] };
34
override readonly rootState;
35
36
readonly dispatchedActions: StateAction[] = [];
37
38
constructor() {
39
super();
40
const self = this;
41
this.rootState = {
42
get value(): RootState { return self._rootStateValue; },
43
get verifiedValue(): RootState { return self._rootStateValue; },
44
onDidChange: Event.None,
45
onWillApplyAction: Event.None,
46
onDidApplyAction: Event.None,
47
};
48
}
49
50
setRootState(rootState: RootState): void {
51
this._rootStateValue = rootState;
52
}
53
54
override dispatch(action: StateAction): void {
55
this.dispatchedActions.push(action);
56
}
57
58
fireAction(envelope: ActionEnvelope): void {
59
this._onDidAction.fire(envelope);
60
}
61
62
dispose(): void {
63
this._onDidAction.dispose();
64
}
65
}
66
67
function createNotificationService(): INotificationService {
68
return new class extends mock<INotificationService>() {
69
override error(): never {
70
throw new Error('Unexpected notification error');
71
}
72
};
73
}
74
75
function createAgentInfo(customizations: readonly CustomizationRef[]): AgentInfo {
76
return {
77
provider: 'copilotcli',
78
displayName: 'Copilot',
79
description: 'Test Agent',
80
models: [],
81
customizations: [...customizations],
82
};
83
}
84
85
suite('RemoteAgentHostCustomizationHarness', () => {
86
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
87
88
test('removeConfiguredPlugin keeps sibling scopes for the same URI', async () => {
89
const connection = disposables.add(new MockAgentConnection());
90
const controller = disposables.add(new RemoteAgentPluginController(
91
'Test Host',
92
'test-authority',
93
connection,
94
{} as IFileDialogService,
95
createNotificationService(),
96
{} as IAICustomizationWorkspaceService,
97
));
98
const pluginA: CustomizationRef = { uri: 'file:///plugins/shared', displayName: 'Shared Plugin' };
99
const pluginB: CustomizationRef = {
100
uri: 'file:///plugins/other',
101
displayName: 'Other Plugin',
102
};
103
connection.setRootState({
104
agents: [],
105
config: {
106
schema: { type: 'object', properties: {} },
107
values: { customizations: [pluginA, pluginB] },
108
},
109
});
110
111
await controller.removeConfiguredPlugin(pluginA);
112
113
assert.deepStrictEqual(connection.dispatchedActions, [{
114
type: ActionType.RootConfigChanged,
115
config: {
116
customizations: [pluginB],
117
},
118
}]);
119
});
120
121
test('provider assigns distinct item keys to plugins with different URIs', async () => {
122
const connection = disposables.add(new MockAgentConnection());
123
const controller = disposables.add(new RemoteAgentPluginController(
124
'Test Host',
125
'test-authority',
126
connection,
127
{} as IFileDialogService,
128
createNotificationService(),
129
{} as IAICustomizationWorkspaceService,
130
));
131
const pluginA: CustomizationRef = { uri: 'file:///plugins/a', displayName: 'Plugin A' };
132
const pluginB: CustomizationRef = { uri: 'file:///plugins/b', displayName: 'Plugin B' };
133
134
connection.setRootState({
135
agents: [createAgentInfo([pluginA, pluginB])],
136
});
137
138
const fileService = new class extends mock<IFileService>() {
139
override async canHandleResource() { return false; }
140
override async resolveAll() { return []; }
141
};
142
143
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
144
createAgentInfo([pluginA, pluginB]),
145
connection,
146
'test-authority',
147
controller,
148
fileService,
149
new NullLogService(),
150
));
151
152
const items = await provider.provideChatSessionCustomizations(CancellationToken.None);
153
assert.strictEqual(items.length, 2);
154
assert.notStrictEqual(items[0].itemKey, items[1].itemKey);
155
});
156
157
test('provider keeps client-synced entries distinct from host-owned entries', async () => {
158
const connection = disposables.add(new MockAgentConnection());
159
const controller = disposables.add(new RemoteAgentPluginController(
160
'Test Host',
161
'test-authority',
162
connection,
163
{} as IFileDialogService,
164
createNotificationService(),
165
{} as IAICustomizationWorkspaceService,
166
));
167
const hostScoped: CustomizationRef = { uri: 'file:///plugins/shared', displayName: 'Shared Plugin' };
168
const synced: SessionCustomization = {
169
customization: hostScoped,
170
clientId: 'test-client',
171
enabled: true,
172
};
173
174
connection.setRootState({
175
agents: [createAgentInfo([hostScoped])],
176
});
177
178
const fileService = new class extends mock<IFileService>() {
179
override async canHandleResource() { return false; }
180
override async resolveAll() { return []; }
181
};
182
183
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
184
createAgentInfo([hostScoped]),
185
connection,
186
'test-authority',
187
controller,
188
fileService,
189
new NullLogService(),
190
));
191
192
connection.fireAction({
193
serverSeq: 1,
194
origin: undefined,
195
action: {
196
type: ActionType.SessionCustomizationsChanged,
197
session: 'agent://copilotcli/session-1',
198
customizations: [synced],
199
},
200
});
201
202
const items = await provider.provideChatSessionCustomizations(CancellationToken.None);
203
assert.strictEqual(items.length, 2);
204
assert.notStrictEqual(items[0].itemKey, items[1].itemKey);
205
});
206
207
test('provider assigns client group to client-synced entries and host group to host entries', async () => {
208
const connection = disposables.add(new MockAgentConnection());
209
const controller = disposables.add(new RemoteAgentPluginController(
210
'Test Host',
211
'test-authority',
212
connection,
213
{} as IFileDialogService,
214
createNotificationService(),
215
{} as IAICustomizationWorkspaceService,
216
));
217
const hostPlugin: CustomizationRef = { uri: 'file:///plugins/host-plugin', displayName: 'Host Plugin' };
218
const clientPlugin: CustomizationRef = { uri: 'file:///plugins/client-plugin', displayName: 'Client Plugin' };
219
const synced: SessionCustomization = {
220
customization: clientPlugin,
221
clientId: 'test-client',
222
enabled: true,
223
};
224
225
connection.setRootState({
226
agents: [createAgentInfo([hostPlugin])],
227
});
228
229
const fileService = new class extends mock<IFileService>() {
230
override async canHandleResource() { return false; }
231
override async resolveAll() { return []; }
232
};
233
234
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
235
createAgentInfo([hostPlugin]),
236
connection,
237
'test-authority',
238
controller,
239
fileService,
240
new NullLogService(),
241
));
242
243
connection.fireAction({
244
serverSeq: 1,
245
origin: undefined,
246
action: {
247
type: ActionType.SessionCustomizationsChanged,
248
session: 'agent://copilotcli/session-1',
249
customizations: [synced],
250
},
251
});
252
253
const items = await provider.provideChatSessionCustomizations(CancellationToken.None);
254
assert.strictEqual(items.length, 2);
255
256
const hostItem = items.find(i => i.name === 'Host Plugin');
257
const clientItem = items.find(i => i.name === 'Client Plugin');
258
assert.ok(hostItem, 'should have a host item');
259
assert.ok(clientItem, 'should have a client item');
260
assert.strictEqual(hostItem.groupKey, 'remote-host');
261
assert.strictEqual(clientItem.groupKey, 'remote-client');
262
});
263
264
test('provider hides synthetic bundle but still expands its contents', async () => {
265
const connection = disposables.add(new MockAgentConnection());
266
const controller = disposables.add(new RemoteAgentPluginController(
267
'Test Host',
268
'test-authority',
269
connection,
270
{} as IFileDialogService,
271
createNotificationService(),
272
{} as IAICustomizationWorkspaceService,
273
));
274
275
const bundleUri = `${SYNCED_CUSTOMIZATION_SCHEME}:///test-authority`;
276
const bundleRef: CustomizationRef = { uri: bundleUri, displayName: 'VS Code Synced Data', nonce: 'abc' };
277
const synced: SessionCustomization = {
278
customization: bundleRef,
279
clientId: 'test-client',
280
enabled: true,
281
status: CustomizationStatus.Loaded,
282
};
283
284
connection.setRootState({ agents: [createAgentInfo([])] });
285
286
// Mock file service that returns a skills directory with one child
287
const skillFileUri = URI.parse(`${bundleUri}/skills/my-skill`);
288
const fileService = new class extends mock<IFileService>() {
289
override async canHandleResource() { return true; }
290
override async resolveAll(resources: { resource: URI }[]): Promise<IFileStatResult[]> {
291
return resources.map(r => {
292
if (r.resource.path.endsWith('/skills')) {
293
return {
294
success: true,
295
stat: {
296
resource: r.resource,
297
name: 'skills',
298
isFile: false,
299
isDirectory: true,
300
isSymbolicLink: false,
301
readonly: false,
302
mtime: 0,
303
ctime: 0,
304
size: 0,
305
children: [{
306
name: 'my-skill',
307
resource: skillFileUri,
308
isFile: false,
309
isDirectory: true,
310
isSymbolicLink: false,
311
readonly: false,
312
mtime: 0,
313
ctime: 0,
314
size: 0,
315
children: [],
316
}],
317
},
318
} satisfies IFileStatResult;
319
}
320
return { success: false, stat: undefined } as unknown as IFileStatResult;
321
});
322
}
323
override async readFile(resource: URI): Promise<IFileContent> {
324
if (resource.path.endsWith('/my-skill/SKILL.md')) {
325
const content = '---\n---\n';
326
return { resource, name: 'SKILL.md', value: VSBuffer.fromString(content), mtime: 0, ctime: 0, etag: '', size: content.length, readonly: false, locked: false, executable: false };
327
}
328
throw new Error('ENOENT');
329
}
330
};
331
332
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
333
createAgentInfo([]),
334
connection,
335
'test-authority',
336
controller,
337
fileService,
338
new NullLogService(),
339
));
340
341
connection.fireAction({
342
serverSeq: 1,
343
origin: undefined,
344
action: {
345
type: ActionType.SessionCustomizationsChanged,
346
session: 'agent://copilotcli/session-1',
347
customizations: [synced],
348
},
349
});
350
351
const items = await provider.provideChatSessionCustomizations(CancellationToken.None);
352
// The synthetic bundle itself should NOT appear as a top-level item
353
assert.ok(!items.some(i => i.name === 'VS Code Synced Data'), 'synthetic bundle should be hidden');
354
// But its expanded child should appear
355
const skillItem = items.find(i => i.name === 'my-skill');
356
assert.ok(skillItem, 'expanded skill from bundle should be present');
357
assert.strictEqual(skillItem.groupKey, 'remote-client', 'expanded children from bundle should be in client group');
358
});
359
360
test('toRemoteUri preserves synced-customization scheme URIs', async () => {
361
const connection = disposables.add(new MockAgentConnection());
362
const controller = disposables.add(new RemoteAgentPluginController(
363
'Test Host',
364
'test-authority',
365
connection,
366
{} as IFileDialogService,
367
createNotificationService(),
368
{} as IAICustomizationWorkspaceService,
369
));
370
371
const bundleUri = `${SYNCED_CUSTOMIZATION_SCHEME}:///test-authority`;
372
const bundleRef: CustomizationRef = { uri: bundleUri, displayName: 'VS Code Synced Data', nonce: 'abc' };
373
const synced: SessionCustomization = {
374
customization: bundleRef,
375
clientId: 'test-client',
376
enabled: true,
377
};
378
379
connection.setRootState({ agents: [createAgentInfo([])] });
380
381
const fileService = new class extends mock<IFileService>() {
382
override async canHandleResource() { return false; }
383
override async resolveAll() { return []; }
384
};
385
386
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
387
createAgentInfo([]),
388
connection,
389
'test-authority',
390
controller,
391
fileService,
392
new NullLogService(),
393
));
394
395
connection.fireAction({
396
serverSeq: 1,
397
origin: undefined,
398
action: {
399
type: ActionType.SessionCustomizationsChanged,
400
session: 'agent://copilotcli/session-1',
401
customizations: [synced],
402
},
403
});
404
405
const items = await provider.provideChatSessionCustomizations(CancellationToken.None);
406
// No top-level item (bundle is hidden), but check that plugin expansion
407
// attempted with the original scheme — not agent-host://
408
// This is verified indirectly: canHandleResource returns false so
409
// no children are produced, but importantly no crash occurred
410
// (toAgentHostUri would throw for this scheme).
411
assert.strictEqual(items.length, 0);
412
});
413
414
test('provider propagates status and enabled from session customizations', async () => {
415
const connection = disposables.add(new MockAgentConnection());
416
const controller = disposables.add(new RemoteAgentPluginController(
417
'Test Host',
418
'test-authority',
419
connection,
420
{} as IFileDialogService,
421
createNotificationService(),
422
{} as IAICustomizationWorkspaceService,
423
));
424
425
const pluginRef: CustomizationRef = { uri: 'file:///plugins/my-plugin', displayName: 'My Plugin' };
426
const sessionCustomization: SessionCustomization = {
427
customization: pluginRef,
428
enabled: false,
429
status: CustomizationStatus.Error,
430
statusMessage: 'something went wrong',
431
};
432
433
connection.setRootState({ agents: [createAgentInfo([pluginRef])] });
434
435
const fileService = new class extends mock<IFileService>() {
436
override async canHandleResource() { return false; }
437
override async resolveAll() { return []; }
438
};
439
440
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
441
createAgentInfo([pluginRef]),
442
connection,
443
'test-authority',
444
controller,
445
fileService,
446
new NullLogService(),
447
));
448
449
connection.fireAction({
450
serverSeq: 1,
451
origin: undefined,
452
action: {
453
type: ActionType.SessionCustomizationsChanged,
454
session: 'agent://copilotcli/session-1',
455
customizations: [sessionCustomization],
456
},
457
});
458
459
const items = await provider.provideChatSessionCustomizations(CancellationToken.None);
460
// Host-scoped plugin from root + session customization → merged into one entry
461
// The session customization entry updates status/statusMessage
462
const sessionItem = items.find(i => i.status === 'error');
463
assert.ok(sessionItem, 'should have an item with error status');
464
assert.strictEqual(sessionItem.statusMessage, 'something went wrong');
465
});
466
467
test('provider fires change event on SessionCustomizationsChanged action', async () => {
468
const connection = disposables.add(new MockAgentConnection());
469
const controller = disposables.add(new RemoteAgentPluginController(
470
'Test Host',
471
'test-authority',
472
connection,
473
{} as IFileDialogService,
474
createNotificationService(),
475
{} as IAICustomizationWorkspaceService,
476
));
477
478
const pluginRef: CustomizationRef = { uri: 'file:///plugins/host', displayName: 'Host Plugin' };
479
connection.setRootState({ agents: [createAgentInfo([pluginRef])] });
480
481
const fileService = new class extends mock<IFileService>() {
482
override async canHandleResource() { return false; }
483
override async resolveAll() { return []; }
484
};
485
486
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
487
createAgentInfo([pluginRef]),
488
connection,
489
'test-authority',
490
controller,
491
fileService,
492
new NullLogService(),
493
));
494
495
let changeCount = 0;
496
disposables.add(provider.onDidChange(() => changeCount++));
497
498
connection.fireAction({
499
serverSeq: 1,
500
origin: undefined,
501
action: {
502
type: ActionType.SessionCustomizationsChanged,
503
session: 'agent://copilotcli/session-1',
504
customizations: [{
505
customization: pluginRef,
506
enabled: true,
507
}],
508
},
509
});
510
511
assert.strictEqual(changeCount, 1, 'should fire change event on session customization action');
512
});
513
514
test('provider does not show remove action for client-synced plugins', async () => {
515
const connection = disposables.add(new MockAgentConnection());
516
const controller = disposables.add(new RemoteAgentPluginController(
517
'Test Host',
518
'test-authority',
519
connection,
520
{} as IFileDialogService,
521
createNotificationService(),
522
{} as IAICustomizationWorkspaceService,
523
));
524
525
const hostPlugin: CustomizationRef = { uri: 'file:///plugins/host', displayName: 'Host Plugin' };
526
const clientPlugin: CustomizationRef = { uri: 'file:///plugins/client', displayName: 'Client Plugin' };
527
528
connection.setRootState({ agents: [createAgentInfo([hostPlugin])] });
529
530
const fileService = new class extends mock<IFileService>() {
531
override async canHandleResource() { return false; }
532
override async resolveAll() { return []; }
533
};
534
535
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
536
createAgentInfo([hostPlugin]),
537
connection,
538
'test-authority',
539
controller,
540
fileService,
541
new NullLogService(),
542
));
543
544
connection.fireAction({
545
serverSeq: 1,
546
origin: undefined,
547
action: {
548
type: ActionType.SessionCustomizationsChanged,
549
session: 'agent://copilotcli/session-1',
550
customizations: [{
551
customization: clientPlugin,
552
clientId: 'test-client',
553
enabled: true,
554
}],
555
},
556
});
557
558
const items = await provider.provideChatSessionCustomizations(CancellationToken.None);
559
const hostItem = items.find(i => i.name === 'Host Plugin');
560
const clientItem = items.find(i => i.name === 'Client Plugin');
561
562
assert.ok(hostItem, 'should have host item');
563
assert.ok(clientItem, 'should have client item');
564
assert.ok(hostItem.actions && hostItem.actions.length > 0, 'host item should have remove action');
565
assert.strictEqual(clientItem.actions, undefined, 'client item should have no actions');
566
});
567
568
test('removeConfiguredPlugin dispatches updated list without the removed plugin', async () => {
569
const connection = disposables.add(new MockAgentConnection());
570
const controller = disposables.add(new RemoteAgentPluginController(
571
'Test Host',
572
'test-authority',
573
connection,
574
{} as IFileDialogService,
575
createNotificationService(),
576
{} as IAICustomizationWorkspaceService,
577
));
578
579
const pluginA: CustomizationRef = { uri: 'file:///plugins/a', displayName: 'Plugin A' };
580
const pluginB: CustomizationRef = { uri: 'file:///plugins/b', displayName: 'Plugin B' };
581
const pluginC: CustomizationRef = { uri: 'file:///plugins/c', displayName: 'Plugin C' };
582
583
connection.setRootState({
584
agents: [],
585
config: {
586
schema: { type: 'object', properties: {} },
587
values: { customizations: [pluginA, pluginB, pluginC] },
588
},
589
});
590
591
await controller.removeConfiguredPlugin(pluginB);
592
593
assert.strictEqual(connection.dispatchedActions.length, 1);
594
assert.deepStrictEqual(connection.dispatchedActions[0], {
595
type: ActionType.RootConfigChanged,
596
config: {
597
customizations: [pluginA, pluginC],
598
},
599
});
600
});
601
602
test('multiple client-synced entries all appear with distinct keys', async () => {
603
const connection = disposables.add(new MockAgentConnection());
604
const controller = disposables.add(new RemoteAgentPluginController(
605
'Test Host',
606
'test-authority',
607
connection,
608
{} as IFileDialogService,
609
createNotificationService(),
610
{} as IAICustomizationWorkspaceService,
611
));
612
613
const clientA: CustomizationRef = { uri: 'file:///plugins/client-a', displayName: 'Client A' };
614
const clientB: CustomizationRef = { uri: 'file:///plugins/client-b', displayName: 'Client B' };
615
616
connection.setRootState({ agents: [createAgentInfo([])] });
617
618
const fileService = new class extends mock<IFileService>() {
619
override async canHandleResource() { return false; }
620
override async resolveAll() { return []; }
621
};
622
623
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
624
createAgentInfo([]),
625
connection,
626
'test-authority',
627
controller,
628
fileService,
629
new NullLogService(),
630
));
631
632
connection.fireAction({
633
serverSeq: 1,
634
origin: undefined,
635
action: {
636
type: ActionType.SessionCustomizationsChanged,
637
session: 'agent://copilotcli/session-1',
638
customizations: [
639
{ customization: clientA, clientId: 'test-client', enabled: true },
640
{ customization: clientB, clientId: 'test-client', enabled: true },
641
],
642
},
643
});
644
645
const items = await provider.provideChatSessionCustomizations(CancellationToken.None);
646
assert.strictEqual(items.length, 2);
647
assert.ok(items.find(i => i.name === 'Client A'), 'should have Client A');
648
assert.ok(items.find(i => i.name === 'Client B'), 'should have Client B');
649
const keys = items.map(i => i.itemKey);
650
assert.strictEqual(new Set(keys).size, 2, 'all item keys should be unique');
651
});
652
653
test('provider parses skill metadata, rewrites folder URIs to SKILL.md, and skips unreadable folder skills', async () => {
654
const connection = disposables.add(new MockAgentConnection());
655
const controller = disposables.add(new RemoteAgentPluginController(
656
'Test Host',
657
'test-authority',
658
connection,
659
{} as IFileDialogService,
660
createNotificationService(),
661
{} as IAICustomizationWorkspaceService,
662
));
663
const plugin: CustomizationRef = { uri: 'file:///plugins/skills-bundle', displayName: 'Skills Bundle' };
664
665
connection.setRootState({ agents: [createAgentInfo([plugin])] });
666
667
// Build a synthetic plugin that contains a `skills/` directory with:
668
// - `valid-skill/` folder (SKILL.md parses with name + description)
669
// - `broken-skill/` folder (SKILL.md read fails — entry should be skipped)
670
// - `legacy.skill.md` flat file (kept as-is, name from filename)
671
const skillsDirChildren: IFileStat[] = [
672
{ name: 'valid-skill', resource: URI.parse('vscode-agent-host://test/plugins/skills-bundle/skills/valid-skill'), isFile: false, isDirectory: true, isSymbolicLink: false, children: undefined },
673
{ name: 'broken-skill', resource: URI.parse('vscode-agent-host://test/plugins/skills-bundle/skills/broken-skill'), isFile: false, isDirectory: true, isSymbolicLink: false, children: undefined },
674
{ name: 'legacy.skill.md', resource: URI.parse('vscode-agent-host://test/plugins/skills-bundle/skills/legacy.skill.md'), isFile: true, isDirectory: false, isSymbolicLink: false, children: undefined },
675
];
676
677
const fileService = new class extends mock<IFileService>() {
678
override async canHandleResource() { return true; }
679
override async resolveAll(toResolve: { resource: URI }[]): Promise<IFileStatResult[]> {
680
return toResolve.map(({ resource }) => {
681
if (resource.path.endsWith('/skills')) {
682
return {
683
success: true,
684
stat: { name: 'skills', resource, isFile: false, isDirectory: true, isSymbolicLink: false, children: skillsDirChildren },
685
};
686
}
687
return { success: false };
688
});
689
}
690
override async readFile(resource: URI): Promise<IFileContent> {
691
if (resource.path.endsWith('/valid-skill/SKILL.md')) {
692
const content = '---\nname: Pretty Name\ndescription: A friendly skill description\n---\n\n# Body\n';
693
return { resource, name: 'SKILL.md', value: VSBuffer.fromString(content), mtime: 0, ctime: 0, etag: '', size: content.length, readonly: false, locked: false, executable: false };
694
}
695
throw new Error('ENOENT');
696
}
697
};
698
699
const provider = disposables.add(new RemoteAgentCustomizationItemProvider(
700
createAgentInfo([plugin]),
701
connection,
702
'test-authority',
703
controller,
704
fileService,
705
new NullLogService(),
706
));
707
708
const items = await provider.provideChatSessionCustomizations(CancellationToken.None);
709
710
const skillItems = items.filter(i => i.type === PromptsType.skill);
711
assert.deepStrictEqual(
712
skillItems.map(i => ({ name: i.name, description: i.description, uri: i.uri.toString() })).sort((a, b) => a.name.localeCompare(b.name)),
713
[
714
{ name: 'Pretty Name', description: 'A friendly skill description', uri: 'vscode-agent-host://test/plugins/skills-bundle/skills/valid-skill/SKILL.md' },
715
{ name: 'legacy', description: undefined, uri: 'vscode-agent-host://test/plugins/skills-bundle/skills/legacy.skill.md' },
716
].sort((a, b) => a.name.localeCompare(b.name)),
717
);
718
});
719
});
720
721