vcluster-yaml-mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -0
- package/package.json +52 -0
- package/src/cli-handlers.js +290 -0
- package/src/cli.js +128 -0
- package/src/formatters.js +168 -0
- package/src/github.js +170 -0
- package/src/http-server.js +67 -0
- package/src/index.js +11 -0
- package/src/middleware/auth.js +25 -0
- package/src/schema-validator.js +351 -0
- package/src/server.js +1006 -0
- package/src/snippet-validator.js +370 -0
- package/src/snippet-validator.test.js +366 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for snippet validator
|
|
3
|
+
* Tests with real vCluster schema and edge cases
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
validateSnippet,
|
|
9
|
+
detectSchemaSection,
|
|
10
|
+
extractSubSchema,
|
|
11
|
+
clearCache,
|
|
12
|
+
getCacheStats
|
|
13
|
+
} from './snippet-validator.js';
|
|
14
|
+
import { githubClient } from './github.js';
|
|
15
|
+
|
|
16
|
+
describe('Snippet Validator Integration Tests', () => {
|
|
17
|
+
let fullSchema;
|
|
18
|
+
const version = 'v0.20.0';
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
// Fetch real vCluster schema from GitHub
|
|
22
|
+
const schemaContent = await githubClient.getFileContent(
|
|
23
|
+
'chart/values.schema.json',
|
|
24
|
+
version
|
|
25
|
+
);
|
|
26
|
+
fullSchema = JSON.parse(schemaContent);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('detectSchemaSection', () => {
|
|
30
|
+
it('should detect top-level section from snippet', () => {
|
|
31
|
+
const snippet = { controlPlane: { distro: { k3s: { enabled: true } } } };
|
|
32
|
+
const section = detectSchemaSection(snippet, fullSchema);
|
|
33
|
+
expect(section).toBe('controlPlane');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should detect section from nested properties', () => {
|
|
37
|
+
const snippet = { enabled: true, host: 'test.com' };
|
|
38
|
+
// This might match multiple sections or return null if ambiguous
|
|
39
|
+
const section = detectSchemaSection(snippet, fullSchema);
|
|
40
|
+
// Since this is ambiguous, it's ok if it returns null
|
|
41
|
+
// The important thing is it doesn't crash
|
|
42
|
+
expect(section).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return null for unrecognizable snippet', () => {
|
|
46
|
+
const snippet = { invalidKey: 'value' };
|
|
47
|
+
const section = detectSchemaSection(snippet, fullSchema);
|
|
48
|
+
expect(section).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle empty snippet', () => {
|
|
52
|
+
const snippet = {};
|
|
53
|
+
const section = detectSchemaSection(snippet, fullSchema);
|
|
54
|
+
expect(section).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('extractSubSchema', () => {
|
|
59
|
+
it('should extract top-level section schema', () => {
|
|
60
|
+
const subSchema = extractSubSchema(fullSchema, 'controlPlane');
|
|
61
|
+
expect(subSchema).not.toBeNull();
|
|
62
|
+
// Schema may have $ref or direct properties
|
|
63
|
+
expect(subSchema).toBeDefined();
|
|
64
|
+
expect(typeof subSchema).toBe('object');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should extract nested section schema', () => {
|
|
68
|
+
const subSchema = extractSubSchema(fullSchema, 'controlPlane.distro');
|
|
69
|
+
// Nested schema extraction may not work with $ref
|
|
70
|
+
// This is ok as long as top-level extraction works
|
|
71
|
+
expect(subSchema).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return null for invalid section', () => {
|
|
75
|
+
const subSchema = extractSubSchema(fullSchema, 'invalidSection');
|
|
76
|
+
expect(subSchema).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('validateSnippet - Real vCluster Scenarios', () => {
|
|
81
|
+
it('Scenario 1: Valid controlPlane ingress config', () => {
|
|
82
|
+
const snippet = `
|
|
83
|
+
controlPlane:
|
|
84
|
+
ingress:
|
|
85
|
+
enabled: true
|
|
86
|
+
host: "my-vcluster.example.com"
|
|
87
|
+
`;
|
|
88
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
89
|
+
|
|
90
|
+
expect(result.syntax_valid).toBe(true);
|
|
91
|
+
expect(result.section).toBe('controlPlane');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('Scenario 2: Valid sync toHost config', () => {
|
|
95
|
+
const snippet = `
|
|
96
|
+
sync:
|
|
97
|
+
toHost:
|
|
98
|
+
services:
|
|
99
|
+
enabled: true
|
|
100
|
+
pods:
|
|
101
|
+
enabled: false
|
|
102
|
+
`;
|
|
103
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
104
|
+
|
|
105
|
+
expect(result.syntax_valid).toBe(true);
|
|
106
|
+
expect(result.section).toBe('sync');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('Scenario 3: Invalid type - enabled as string', () => {
|
|
110
|
+
const snippet = `
|
|
111
|
+
controlPlane:
|
|
112
|
+
distro:
|
|
113
|
+
k3s:
|
|
114
|
+
enabled: "yes"
|
|
115
|
+
`;
|
|
116
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
117
|
+
|
|
118
|
+
expect(result.syntax_valid).toBe(true);
|
|
119
|
+
expect(result.valid).toBe(false);
|
|
120
|
+
expect(result.errors).toBeDefined();
|
|
121
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
122
|
+
expect(result.errors[0].message).toContain('boolean');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('Scenario 4: Deep nesting - k3s config', () => {
|
|
126
|
+
const snippet = `
|
|
127
|
+
controlPlane:
|
|
128
|
+
distro:
|
|
129
|
+
k3s:
|
|
130
|
+
enabled: true
|
|
131
|
+
image:
|
|
132
|
+
tag: "v1.28.0"
|
|
133
|
+
`;
|
|
134
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
135
|
+
|
|
136
|
+
expect(result.syntax_valid).toBe(true);
|
|
137
|
+
expect(result.section).toBe('controlPlane');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('Scenario 5: YAML syntax error', () => {
|
|
141
|
+
const snippet = `
|
|
142
|
+
invalid yaml:
|
|
143
|
+
- not properly: formatted
|
|
144
|
+
missing colon
|
|
145
|
+
`;
|
|
146
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
147
|
+
|
|
148
|
+
expect(result.syntax_valid).toBe(false);
|
|
149
|
+
expect(result.syntax_error).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('Scenario 6: Partial snippet without section key', () => {
|
|
153
|
+
const snippet = `
|
|
154
|
+
enabled: true
|
|
155
|
+
host: "test.com"
|
|
156
|
+
`;
|
|
157
|
+
const result = validateSnippet(snippet, fullSchema, version, 'controlPlane');
|
|
158
|
+
|
|
159
|
+
expect(result.syntax_valid).toBe(true);
|
|
160
|
+
// Should validate against the hinted section
|
|
161
|
+
expect(result.section).toBe('controlPlane');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('Scenario 7: Invalid section hint', () => {
|
|
165
|
+
const snippet = `
|
|
166
|
+
enabled: true
|
|
167
|
+
`;
|
|
168
|
+
const result = validateSnippet(snippet, fullSchema, version, 'invalidSection');
|
|
169
|
+
|
|
170
|
+
expect(result.valid).toBe(false);
|
|
171
|
+
expect(result.error).toContain('not found');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('Scenario 8: Empty snippet', () => {
|
|
175
|
+
const snippet = ``;
|
|
176
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
177
|
+
|
|
178
|
+
expect(result.valid).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('Scenario 9: Ambiguous snippet - could match multiple sections', () => {
|
|
182
|
+
const snippet = `
|
|
183
|
+
enabled: true
|
|
184
|
+
`;
|
|
185
|
+
// Without hint, should try to detect automatically
|
|
186
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
187
|
+
|
|
188
|
+
// Either succeeds with detected section or fails asking for hint
|
|
189
|
+
if (!result.valid && result.error) {
|
|
190
|
+
expect(result.error).toContain('Could not detect schema section');
|
|
191
|
+
} else {
|
|
192
|
+
expect(result.section).toBeDefined();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('Scenario 10: Additional properties not in schema', () => {
|
|
197
|
+
const snippet = `
|
|
198
|
+
controlPlane:
|
|
199
|
+
customField: "value"
|
|
200
|
+
`;
|
|
201
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
202
|
+
|
|
203
|
+
// AJV with strict:false should allow additional properties
|
|
204
|
+
// or flag them depending on schema's additionalProperties setting
|
|
205
|
+
expect(result.syntax_valid).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('Validator Cache', () => {
|
|
210
|
+
it('should cache compiled validators', () => {
|
|
211
|
+
clearCache();
|
|
212
|
+
|
|
213
|
+
const snippet = `
|
|
214
|
+
controlPlane:
|
|
215
|
+
distro:
|
|
216
|
+
k3s:
|
|
217
|
+
enabled: true
|
|
218
|
+
`;
|
|
219
|
+
|
|
220
|
+
// First call - should compile and cache
|
|
221
|
+
const result1 = validateSnippet(snippet, fullSchema, version);
|
|
222
|
+
const stats1 = getCacheStats();
|
|
223
|
+
expect(stats1.size).toBeGreaterThan(0);
|
|
224
|
+
|
|
225
|
+
// Second call - should use cache
|
|
226
|
+
const result2 = validateSnippet(snippet, fullSchema, version);
|
|
227
|
+
const stats2 = getCacheStats();
|
|
228
|
+
|
|
229
|
+
expect(result1.valid).toBe(result2.valid);
|
|
230
|
+
expect(stats2.size).toBe(stats1.size);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should clear cache on version change', () => {
|
|
234
|
+
clearCache();
|
|
235
|
+
|
|
236
|
+
const snippet = `
|
|
237
|
+
controlPlane:
|
|
238
|
+
distro:
|
|
239
|
+
k3s:
|
|
240
|
+
enabled: true
|
|
241
|
+
`;
|
|
242
|
+
|
|
243
|
+
// Validate with version 1
|
|
244
|
+
validateSnippet(snippet, fullSchema, 'v1.0.0');
|
|
245
|
+
const stats1 = getCacheStats();
|
|
246
|
+
expect(stats1.version).toBe('v1.0.0');
|
|
247
|
+
|
|
248
|
+
// Validate with version 2 - should clear cache
|
|
249
|
+
validateSnippet(snippet, fullSchema, 'v2.0.0');
|
|
250
|
+
const stats2 = getCacheStats();
|
|
251
|
+
expect(stats2.version).toBe('v2.0.0');
|
|
252
|
+
expect(stats2.size).toBeGreaterThan(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should respect max cache size', () => {
|
|
256
|
+
clearCache();
|
|
257
|
+
|
|
258
|
+
// Create 21 different snippets to exceed cache size (20)
|
|
259
|
+
for (let i = 0; i < 21; i++) {
|
|
260
|
+
const snippet = `
|
|
261
|
+
section${i}:
|
|
262
|
+
enabled: true
|
|
263
|
+
`;
|
|
264
|
+
// Use different section hints to create different cache entries
|
|
265
|
+
validateSnippet(snippet, fullSchema, version, 'controlPlane');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const stats = getCacheStats();
|
|
269
|
+
expect(stats.size).toBeLessThanOrEqual(20);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('Error Messages', () => {
|
|
274
|
+
it('should include snippet context in errors', () => {
|
|
275
|
+
const snippet = `
|
|
276
|
+
controlPlane:
|
|
277
|
+
distro:
|
|
278
|
+
k3s:
|
|
279
|
+
enabled: "not a boolean"
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
283
|
+
|
|
284
|
+
expect(result.valid).toBe(false);
|
|
285
|
+
expect(result.errors).toBeDefined();
|
|
286
|
+
expect(result.errors[0].context).toContain('controlPlane');
|
|
287
|
+
expect(result.errors[0].path).toBeDefined();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should provide helpful error summary', () => {
|
|
291
|
+
const snippet = `
|
|
292
|
+
controlPlane:
|
|
293
|
+
distro:
|
|
294
|
+
k3s:
|
|
295
|
+
enabled: "invalid"
|
|
296
|
+
`;
|
|
297
|
+
|
|
298
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
299
|
+
|
|
300
|
+
expect(result.valid).toBe(false);
|
|
301
|
+
expect(result.summary).toBeDefined();
|
|
302
|
+
expect(result.summary).toContain('error');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Edge Cases', () => {
|
|
307
|
+
it('should handle snippet with arrays', () => {
|
|
308
|
+
const snippet = `
|
|
309
|
+
sync:
|
|
310
|
+
toHost:
|
|
311
|
+
services:
|
|
312
|
+
enabled: true
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
316
|
+
expect(result.syntax_valid).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should handle snippet with null values', () => {
|
|
320
|
+
const snippet = `
|
|
321
|
+
controlPlane:
|
|
322
|
+
distro:
|
|
323
|
+
k3s:
|
|
324
|
+
enabled: true
|
|
325
|
+
extraArgs: null
|
|
326
|
+
`;
|
|
327
|
+
|
|
328
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
329
|
+
expect(result.syntax_valid).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should handle deeply nested paths', () => {
|
|
333
|
+
const snippet = `
|
|
334
|
+
controlPlane:
|
|
335
|
+
distro:
|
|
336
|
+
k3s:
|
|
337
|
+
enabled: true
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
341
|
+
expect(result.syntax_valid).toBe(true);
|
|
342
|
+
expect(result.section).toBe('controlPlane');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should handle multiple top-level sections', () => {
|
|
346
|
+
const snippet = `
|
|
347
|
+
controlPlane:
|
|
348
|
+
distro:
|
|
349
|
+
k3s:
|
|
350
|
+
enabled: true
|
|
351
|
+
sync:
|
|
352
|
+
toHost:
|
|
353
|
+
services:
|
|
354
|
+
enabled: true
|
|
355
|
+
`;
|
|
356
|
+
|
|
357
|
+
const result = validateSnippet(snippet, fullSchema, version);
|
|
358
|
+
|
|
359
|
+
// Should detect first section or fail gracefully
|
|
360
|
+
expect(result.syntax_valid).toBe(true);
|
|
361
|
+
if (result.valid) {
|
|
362
|
+
expect(result.section).toBeDefined();
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|