Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/agentPluginManager.test.ts
13399 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 { VSBuffer } from '../../../../base/common/buffer.js';
8
import { DisposableStore } from '../../../../base/common/lifecycle.js';
9
import { Schemas } from '../../../../base/common/network.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
12
import { FileService } from '../../../files/common/fileService.js';
13
import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js';
14
import { NullLogService } from '../../../log/common/log.js';
15
import { AGENT_CLIENT_SCHEME, toAgentClientUri } from '../../common/agentClientUri.js';
16
import { CustomizationStatus, type CustomizationRef, type SessionCustomization } from '../../common/state/sessionState.js';
17
import { AgentPluginManager } from '../../node/agentPluginManager.js';
18
19
suite('AgentPluginManager', () => {
20
21
const disposables = new DisposableStore();
22
let fileService: FileService;
23
let manager: AgentPluginManager;
24
const basePath = URI.from({ scheme: Schemas.inMemory, path: '/userData' });
25
26
setup(() => {
27
fileService = disposables.add(new FileService(new NullLogService()));
28
disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider())));
29
disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, disposables.add(new InMemoryFileSystemProvider())));
30
manager = new AgentPluginManager(basePath, fileService, new NullLogService());
31
});
32
33
teardown(() => disposables.clear());
34
ensureNoDisposablesAreLeakedInTestSuite();
35
36
function pluginUri(name: string): string {
37
return URI.from({ scheme: Schemas.inMemory, path: `/plugins/${name}` }).toString();
38
}
39
40
function makeRef(name: string, nonce?: string): CustomizationRef {
41
return { uri: pluginUri(name), displayName: `Plugin ${name}`, nonce };
42
}
43
44
async function seedPluginDir(name: string, files: Record<string, string>): Promise<void> {
45
const originalUri = URI.from({ scheme: Schemas.inMemory, path: `/plugins/${name}` });
46
const agentClientDir = toAgentClientUri(originalUri, 'test-client');
47
await fileService.createFolder(agentClientDir);
48
for (const [fileName, content] of Object.entries(files)) {
49
await fileService.writeFile(URI.joinPath(agentClientDir, fileName), VSBuffer.fromString(content));
50
}
51
}
52
53
// ---- syncCustomizations -------------------------------------------------
54
55
suite('syncCustomizations', () => {
56
57
test('returns loaded status and pluginDir for each synced plugin', async () => {
58
await seedPluginDir('alpha', { 'index.js': 'a' });
59
await seedPluginDir('beta', { 'index.js': 'b' });
60
61
const results = await manager.syncCustomizations('test-client', [
62
makeRef('alpha', 'n1'),
63
makeRef('beta', 'n2'),
64
]);
65
assert.strictEqual(results[0].customization.status, CustomizationStatus.Loaded);
66
assert.ok(results[0].pluginDir, 'should have pluginDir');
67
assert.strictEqual(results[1].customization.status, CustomizationStatus.Loaded);
68
assert.ok(results[1].pluginDir, 'should have pluginDir');
69
});
70
71
test('returns error status without pluginDir when source missing', async () => {
72
const results = await manager.syncCustomizations('test-client', [makeRef('nonexistent')]);
73
74
assert.strictEqual(results.length, 1);
75
assert.strictEqual(results[0].customization.status, CustomizationStatus.Error);
76
assert.ok(results[0].customization.statusMessage);
77
assert.strictEqual(results[0].pluginDir, undefined);
78
});
79
80
test('mixes loaded and error results', async () => {
81
await seedPluginDir('good', { 'index.js': 'ok' });
82
83
const results = await manager.syncCustomizations('test-client', [
84
makeRef('good', 'n1'),
85
makeRef('missing'),
86
]);
87
assert.strictEqual(results[1].customization.status, CustomizationStatus.Error);
88
assert.strictEqual(results[1].pluginDir, undefined);
89
});
90
91
test('fires progress callback with loading, then loaded', async () => {
92
await seedPluginDir('prog', { 'index.js': 'content' });
93
94
const progressCalls: SessionCustomization[][] = [];
95
await manager.syncCustomizations('test-client', [makeRef('prog', 'n1')], statuses => {
96
progressCalls.push(statuses);
97
});
98
99
// At least two calls: initial loading + final loaded
100
assert.ok(progressCalls.length >= 2, `expected at least 2 progress calls, got ${progressCalls.length}`);
101
assert.strictEqual(progressCalls[0][0].status, CustomizationStatus.Loading);
102
assert.strictEqual(progressCalls[progressCalls.length - 1][0].status, CustomizationStatus.Loaded);
103
});
104
105
test('skips copy when nonce matches', async () => {
106
await seedPluginDir('cached', { 'index.js': 'v1' });
107
const ref = makeRef('cached', 'nonce-abc');
108
109
const result1 = await manager.syncCustomizations('test-client', [ref]);
110
assert.ok(result1[0].pluginDir);
111
112
// Second sync with same nonce should still succeed (from cache)
113
const result2 = await manager.syncCustomizations('test-client', [ref]);
114
assert.ok(result2[0].pluginDir);
115
assert.strictEqual(result1[0].pluginDir!.toString(), result2[0].pluginDir!.toString());
116
});
117
118
test('serializes concurrent syncs of the same URI', async () => {
119
await seedPluginDir('concurrent', { 'index.js': 'v1' });
120
const ref = makeRef('concurrent', 'n1');
121
122
// Fire two syncs concurrently
123
const [r1, r2] = await Promise.all([
124
manager.syncCustomizations('test-client', [ref]),
125
manager.syncCustomizations('test-client', [ref]),
126
]);
127
128
// Both should succeed without error
129
assert.strictEqual(r1[0].customization.status, CustomizationStatus.Loaded);
130
assert.strictEqual(r2[0].customization.status, CustomizationStatus.Loaded);
131
});
132
});
133
134
// ---- LRU eviction -------------------------------------------------------
135
136
suite('LRU eviction', () => {
137
138
test('evicts least recently used plugins when limit exceeded', async () => {
139
const smallManager = new AgentPluginManager(basePath, fileService, new NullLogService(), 3);
140
141
for (let i = 1; i <= 4; i++) {
142
await seedPluginDir(`plugin-${i}`, { 'index.js': `p${i}` });
143
await smallManager.syncCustomizations('test-client', [makeRef(`plugin-${i}`, `n${i}`)]);
144
}
145
146
// The evicted dir should no longer exist on disk (cache.json + 3 plugin dirs)
147
const evictedDir = URI.joinPath(basePath, 'agentPlugins');
148
const listing = await fileService.resolve(evictedDir);
149
assert.ok(listing.children);
150
const pluginDirs = listing.children.filter(c => c.isDirectory);
151
assert.strictEqual(pluginDirs.length, 3, 'should have exactly 3 plugin dirs after eviction');
152
});
153
});
154
155
// ---- cache persistence --------------------------------------------------
156
157
suite('cache persistence', () => {
158
159
test('restores nonce cache from disk on new manager instance', async () => {
160
await seedPluginDir('persist1', { 'index.js': 'v1' });
161
const ref = makeRef('persist1', 'nonce-persist');
162
163
// Sync with first manager
164
await manager.syncCustomizations('test-client', [ref]);
165
166
// Create a new manager pointing to the same base path
167
const manager2 = new AgentPluginManager(basePath, fileService, new NullLogService());
168
const result = await manager2.syncCustomizations('test-client', [ref]);
169
170
// Should be loaded from cache (nonce match), not error
171
assert.strictEqual(result[0].customization.status, CustomizationStatus.Loaded);
172
assert.ok(result[0].pluginDir);
173
});
174
});
175
});
176
177