Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/common/agentHostSchema.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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
8
import { createSchema, platformSessionSchema, schemaProperty, type AutoApproveLevel, type IPermissionsValue, type SessionMode } from '../../common/agentHostSchema.js';
9
import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
10
import { JsonRpcErrorCodes, ProtocolError } from '../../common/state/sessionProtocol.js';
11
12
/**
13
* Invokes `fn` and returns the thrown {@link ProtocolError}. Avoids
14
* passing an arrow-function validator to `assert.throws` — the unit-test
15
* assert shim does `actual instanceof expected` with that validator, and
16
* arrow functions have no `prototype` property, which WebKit rejects.
17
*/
18
function captureProtocolError(fn: () => void): ProtocolError {
19
try {
20
fn();
21
} catch (err) {
22
assert.ok(err instanceof ProtocolError, `expected ProtocolError, got: ${err}`);
23
return err;
24
}
25
assert.fail('expected fn to throw, but it did not');
26
}
27
28
suite('agentHostSchema', () => {
29
30
ensureNoDisposablesAreLeakedInTestSuite();
31
32
// ---- schemaProperty / individual validators ---------------------------
33
34
suite('schemaProperty', () => {
35
36
test('validates primitive types', () => {
37
const str = schemaProperty<string>({ type: 'string', title: 's' });
38
assert.strictEqual(str.validate('hello'), true);
39
assert.strictEqual(str.validate(42), false);
40
assert.strictEqual(str.validate(undefined), false);
41
assert.strictEqual(str.validate(null), false);
42
43
const num = schemaProperty<number>({ type: 'number', title: 'n' });
44
assert.strictEqual(num.validate(42), true);
45
assert.strictEqual(num.validate('42'), false);
46
47
const bool = schemaProperty<boolean>({ type: 'boolean', title: 'b' });
48
assert.strictEqual(bool.validate(true), true);
49
assert.strictEqual(bool.validate(0), false);
50
});
51
52
test('enforces enum values', () => {
53
const prop = schemaProperty<'a' | 'b'>({
54
type: 'string',
55
title: 'letters',
56
enum: ['a', 'b'],
57
});
58
assert.strictEqual(prop.validate('a'), true);
59
assert.strictEqual(prop.validate('b'), true);
60
assert.strictEqual(prop.validate('c'), false);
61
assert.strictEqual(prop.validate(42), false);
62
});
63
64
test('enumDynamic bypasses enum check but keeps type check', () => {
65
const prop = schemaProperty<string>({
66
type: 'string',
67
title: 'dyn',
68
enum: ['seed'],
69
enumDynamic: true,
70
});
71
assert.strictEqual(prop.validate('seed'), true);
72
assert.strictEqual(prop.validate('anything-else'), true);
73
assert.strictEqual(prop.validate(42), false);
74
});
75
76
test('validates nested objects and required keys', () => {
77
const prop = schemaProperty<{ name: string; age?: number }>({
78
type: 'object',
79
title: 'person',
80
properties: {
81
name: { type: 'string', title: 'name' },
82
age: { type: 'number', title: 'age' },
83
},
84
required: ['name'],
85
});
86
assert.strictEqual(prop.validate({ name: 'alice' }), true);
87
assert.strictEqual(prop.validate({ name: 'alice', age: 30 }), true);
88
assert.strictEqual(prop.validate({ age: 30 }), false);
89
assert.strictEqual(prop.validate({ name: 42 }), false);
90
assert.strictEqual(prop.validate([]), false);
91
assert.strictEqual(prop.validate(null), false);
92
});
93
94
test('validates arrays with item schema', () => {
95
const prop = schemaProperty<string[]>({
96
type: 'array',
97
title: 'names',
98
items: { type: 'string', title: 'name' },
99
});
100
assert.strictEqual(prop.validate(['a', 'b']), true);
101
assert.strictEqual(prop.validate([]), true);
102
assert.strictEqual(prop.validate(['a', 42]), false);
103
assert.strictEqual(prop.validate('a'), false);
104
});
105
106
test('assertValid throws ProtocolError with offending path for primitive mismatch', () => {
107
const prop = schemaProperty<string>({ type: 'string', title: 's' });
108
const err = captureProtocolError(() => prop.assertValid(42, 'myKey'));
109
assert.strictEqual(err.code, JsonRpcErrorCodes.InvalidParams);
110
assert.ok(err.message.includes('myKey'), err.message);
111
assert.ok(err.message.includes('string'), err.message);
112
});
113
114
test('assertValid path annotates array index and nested property', () => {
115
const prop = schemaProperty<{ allow: string[] }>({
116
type: 'object',
117
title: 'perms',
118
properties: {
119
allow: {
120
type: 'array',
121
title: 'allow',
122
items: { type: 'string', title: 'name' },
123
},
124
},
125
});
126
const err = captureProtocolError(() => prop.assertValid({ allow: ['ok', 42] }, 'permissions'));
127
assert.ok(err.message.includes('permissions.allow[1]'), err.message);
128
assert.ok(err.message.includes('string'), err.message);
129
});
130
131
test('assertValid path reports missing required property', () => {
132
const prop = schemaProperty<{ name: string }>({
133
type: 'object',
134
title: 'person',
135
properties: { name: { type: 'string', title: 'name' } },
136
required: ['name'],
137
});
138
const err = captureProtocolError(() => prop.assertValid({}, 'person'));
139
assert.ok(err.message.includes('person.name'), err.message);
140
assert.ok(err.message.toLowerCase().includes('required'), err.message);
141
});
142
143
test('assertValid reports enum violation with the allowed set', () => {
144
const prop = schemaProperty<'a' | 'b'>({
145
type: 'string',
146
title: 'letters',
147
enum: ['a', 'b'],
148
});
149
const err = captureProtocolError(() => prop.assertValid('c', 'choice'));
150
assert.ok(err.message.includes('choice'), err.message);
151
assert.ok(err.message.includes('"a"'), err.message);
152
assert.ok(err.message.includes('"b"'), err.message);
153
});
154
});
155
156
// ---- createSchema ------------------------------------------------------
157
158
suite('createSchema', () => {
159
160
const fixture = () => createSchema({
161
name: schemaProperty<string>({ type: 'string', title: 'name' }),
162
count: schemaProperty<number>({ type: 'number', title: 'count' }),
163
level: schemaProperty<'low' | 'high'>({
164
type: 'string',
165
title: 'level',
166
enum: ['low', 'high'],
167
}),
168
});
169
170
test('toProtocol emits a JSON-Schema-compatible object', () => {
171
const schema = fixture();
172
const protocol = schema.toProtocol();
173
assert.strictEqual(protocol.type, 'object');
174
assert.deepStrictEqual(Object.keys(protocol.properties), ['name', 'count', 'level']);
175
assert.strictEqual(protocol.properties.name.type, 'string');
176
assert.deepStrictEqual(protocol.properties.level.enum, ['low', 'high']);
177
});
178
179
test('validate returns false for unknown keys', () => {
180
const schema = fixture();
181
assert.strictEqual(schema.validate('name', 'ok'), true);
182
assert.strictEqual(schema.validate('name', 42), false);
183
assert.strictEqual(schema.validate('unknown' as 'name', 'ok'), false);
184
});
185
186
test('assertValid throws for unknown keys', () => {
187
const schema = fixture();
188
const err = captureProtocolError(() => schema.assertValid('unknown' as 'name', 'x'));
189
assert.ok(err.message.includes('unknown'), err.message);
190
});
191
192
test('values returns a shallow copy and passes through unknown keys', () => {
193
const schema = fixture();
194
const input = { name: 'alice', count: 3, extra: 'forward-compat' };
195
const out = schema.values(input);
196
assert.notStrictEqual(out, input);
197
assert.deepStrictEqual(out, input);
198
});
199
200
test('values skips undefined entries without throwing', () => {
201
const schema = fixture();
202
const out = schema.values({ name: 'alice' });
203
assert.deepStrictEqual(out, { name: 'alice' });
204
});
205
206
test('values throws a path-annotated ProtocolError on invalid entry', () => {
207
const schema = fixture();
208
const err = captureProtocolError(() => schema.values({ name: 42 as unknown as string }));
209
assert.strictEqual(err.code, JsonRpcErrorCodes.InvalidParams);
210
assert.ok(err.message.includes('name'), err.message);
211
});
212
213
test('definition is preserved for spread-based composition', () => {
214
const base = createSchema({
215
a: schemaProperty<string>({ type: 'string', title: 'a' }),
216
});
217
const extended = createSchema({
218
...base.definition,
219
b: schemaProperty<number>({ type: 'number', title: 'b' }),
220
});
221
assert.deepStrictEqual(Object.keys(extended.toProtocol().properties), ['a', 'b']);
222
assert.strictEqual(extended.validate('a', 'hi'), true);
223
assert.strictEqual(extended.validate('b', 3), true);
224
});
225
});
226
227
// ---- validateOrDefault -------------------------------------------------
228
229
suite('validateOrDefault', () => {
230
231
const fixture = () => createSchema({
232
name: schemaProperty<string>({ type: 'string', title: 'name' }),
233
count: schemaProperty<number>({ type: 'number', title: 'count' }),
234
});
235
236
test('substitutes defaults for missing or invalid values', () => {
237
const schema = fixture();
238
const defaults = { name: 'default', count: 0 };
239
const result = schema.validateOrDefault({ name: 42, count: 5 }, defaults);
240
assert.deepStrictEqual(result, { name: 'default', count: 5 });
241
});
242
243
test('passes through all-valid values', () => {
244
const schema = fixture();
245
const result = schema.validateOrDefault({ name: 'alice', count: 3 }, { name: 'd', count: 0 });
246
assert.deepStrictEqual(result, { name: 'alice', count: 3 });
247
});
248
249
test('uses defaults when input is undefined', () => {
250
const schema = fixture();
251
const result = schema.validateOrDefault(undefined, { name: 'd', count: 7 });
252
assert.deepStrictEqual(result, { name: 'd', count: 7 });
253
});
254
255
test('ignores keys not in defaults', () => {
256
const schema = fixture();
257
// @ts-expect-error: test that extra keys not in the defaults are ignored, even if they pass validation.
258
const result = schema.validateOrDefault({ name: 'a', count: 1, ignored: true }, { name: 'd', count: 0 });
259
assert.deepStrictEqual(result, { name: 'a', count: 1 });
260
});
261
262
test('omits schema keys that are missing from both values and defaults', () => {
263
// Regression coverage for the partial-defaults contract that
264
// underpins host-level inheritance: if the caller doesn't supply
265
// a default and no incoming value is valid, the key is left out
266
// entirely so higher-scope defaults can fill in.
267
const schema = fixture();
268
const result = schema.validateOrDefault({ count: 9 }, { count: 0 });
269
assert.deepStrictEqual(result, { count: 9 });
270
assert.ok(!result.hasOwnProperty('name'), '`name` should be absent when neither values nor defaults supply it');
271
});
272
273
test('omits schema keys when value is invalid and no default is supplied', () => {
274
const schema = fixture();
275
// @ts-expect-error: test that invalid values are dropped even when the caller doesn't provide a default.
276
const result = schema.validateOrDefault({ name: 42, count: 3 }, { count: 0 });
277
// `name` has no default and the incoming value is invalid → dropped.
278
assert.deepStrictEqual(result, { count: 3 });
279
});
280
});
281
282
// ---- platformSessionSchema sanity --------------------------------------
283
284
suite('platformSessionSchema', () => {
285
286
test('validates the three autoApprove levels', () => {
287
const levels: AutoApproveLevel[] = ['default', 'autoApprove', 'autopilot'];
288
for (const level of levels) {
289
assert.strictEqual(platformSessionSchema.validate(SessionConfigKey.AutoApprove, level), true, level);
290
}
291
assert.strictEqual(platformSessionSchema.validate(SessionConfigKey.AutoApprove, 'bogus'), false);
292
});
293
294
test('validates permissions shape', () => {
295
const ok: IPermissionsValue = { allow: ['read'], deny: [] };
296
assert.strictEqual(platformSessionSchema.validate(SessionConfigKey.Permissions, ok), true);
297
assert.strictEqual(platformSessionSchema.validate(SessionConfigKey.Permissions, { allow: [42], deny: [] }), false);
298
assert.strictEqual(platformSessionSchema.validate(SessionConfigKey.Permissions, { allow: [] }), true);
299
});
300
301
test('validates the agent modes', () => {
302
const modes: SessionMode[] = ['interactive', 'plan'];
303
for (const mode of modes) {
304
assert.strictEqual(platformSessionSchema.validate(SessionConfigKey.Mode, mode), true, mode);
305
}
306
// `autopilot` is intentionally NOT in the AHP mode enum \u2014 it's
307
// modeled on the orthogonal `autoApprove` axis instead.
308
assert.strictEqual(platformSessionSchema.validate(SessionConfigKey.Mode, 'autopilot'), false);
309
assert.strictEqual(platformSessionSchema.validate(SessionConfigKey.Mode, 'shell'), false);
310
assert.strictEqual(platformSessionSchema.validate(SessionConfigKey.Mode, 42), false);
311
});
312
});
313
});
314
315