@kya-os/create-mcpi-app 1.7.17 → 1.7.20
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test$colon$coverage.log +315 -0
- package/.turbo/turbo-test.log +95 -0
- package/CHANGELOG.md +372 -0
- package/IMPLEMENTATION_SUMMARY.md +108 -0
- package/REMEDIATION_PLAN.md +99 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +252 -0
- package/coverage/config-builder.ts.html +580 -0
- package/coverage/coverage-final.json +7 -0
- package/coverage/favicon.png +0 -0
- package/coverage/fetch-cloudflare-mcpi-template.ts.html +7006 -0
- package/coverage/generate-config.ts.html +436 -0
- package/coverage/generate-identity.ts.html +574 -0
- package/coverage/index.html +191 -0
- package/coverage/install.ts.html +322 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/validate-project-structure.ts.html +466 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/helpers/__tests__/config-builder.spec.d.ts +8 -0
- package/dist/helpers/__tests__/config-builder.spec.d.ts.map +1 -0
- package/dist/helpers/__tests__/config-builder.spec.js +182 -0
- package/dist/helpers/__tests__/config-builder.spec.js.map +1 -0
- package/dist/helpers/config-builder.d.ts +58 -0
- package/dist/helpers/config-builder.d.ts.map +1 -0
- package/dist/helpers/config-builder.js +102 -0
- package/dist/helpers/config-builder.js.map +1 -0
- package/dist/helpers/create.d.ts +1 -0
- package/dist/helpers/create.d.ts.map +1 -1
- package/dist/helpers/create.js +2 -1
- package/dist/helpers/create.js.map +1 -1
- package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts +1 -0
- package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts.map +1 -1
- package/dist/helpers/fetch-cloudflare-mcpi-template.js +209 -174
- package/dist/helpers/fetch-cloudflare-mcpi-template.js.map +1 -1
- package/dist/helpers/fetch-mcpi-template.d.ts.map +1 -1
- package/dist/helpers/fetch-mcpi-template.js +18 -3
- package/dist/helpers/fetch-mcpi-template.js.map +1 -1
- package/dist/helpers/generate-config.d.ts.map +1 -1
- package/dist/helpers/generate-config.js +27 -40
- package/dist/helpers/generate-config.js.map +1 -1
- package/dist/helpers/install.js +5 -0
- package/dist/helpers/install.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +18 -9
- package/scripts/prepare-pack.js +47 -0
- package/scripts/validate-no-workspace.js +79 -0
- package/src/__tests__/cloudflare-template.test.ts +488 -0
- package/src/__tests__/helpers/fetch-cloudflare-mcpi-template.test.ts +337 -0
- package/src/__tests__/helpers/generate-config.test.ts +312 -0
- package/src/__tests__/helpers/generate-identity.test.ts +271 -0
- package/src/__tests__/helpers/install.test.ts +362 -0
- package/src/__tests__/helpers/validate-project-structure.test.ts +467 -0
- package/src/__tests__.bak/regression.test.ts +434 -0
- package/src/effects/index.ts +80 -0
- package/src/helpers/__tests__/config-builder.spec.ts +231 -0
- package/src/helpers/apply-identity-preset.ts +209 -0
- package/src/helpers/config-builder.ts +165 -0
- package/src/helpers/copy-template.ts +11 -0
- package/src/helpers/create.ts +239 -0
- package/src/helpers/fetch-cloudflare-mcpi-template.ts +2311 -0
- package/src/helpers/fetch-cloudflare-template.ts +361 -0
- package/src/helpers/fetch-mcpi-template.ts +236 -0
- package/src/helpers/fetch-xmcp-template.ts +153 -0
- package/src/helpers/generate-config.ts +117 -0
- package/src/helpers/generate-identity.ts +163 -0
- package/src/helpers/identity-manager.ts +186 -0
- package/src/helpers/install.ts +79 -0
- package/src/helpers/rename.ts +17 -0
- package/src/helpers/validate-project-structure.ts +127 -0
- package/src/index.ts +480 -0
- package/src/utils/check-node.ts +17 -0
- package/src/utils/is-folder-empty.ts +60 -0
- package/src/utils/validate-project-name.ts +132 -0
- package/test-cloudflare/README.md +164 -0
- package/test-cloudflare/package.json +28 -0
- package/test-cloudflare/src/index.ts +340 -0
- package/test-cloudflare/src/tools/greet.ts +19 -0
- package/test-cloudflare/tests/cache-invalidation.test.ts +410 -0
- package/test-cloudflare/tests/cors-security.test.ts +349 -0
- package/test-cloudflare/tests/delegation.test.ts +335 -0
- package/test-cloudflare/tests/do-routing.test.ts +314 -0
- package/test-cloudflare/tests/integration.test.ts +205 -0
- package/test-cloudflare/tests/session-management.test.ts +359 -0
- package/test-cloudflare/tsconfig.json +22 -0
- package/test-cloudflare/vitest.config.ts +9 -0
- package/test-cloudflare/wrangler.toml +37 -0
- package/test-node/README.md +44 -0
- package/test-node/package.json +23 -0
- package/test-node/src/tools/greet.ts +25 -0
- package/test-node/xmcp.config.ts +20 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch Cloudflare MCP-I Template Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for template fetching logic, caching, network failure handling,
|
|
5
|
+
* and template transformation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
9
|
+
import { fetchCloudflareMcpiTemplate } from '../../helpers/fetch-cloudflare-mcpi-template';
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
describe('fetchCloudflareMcpiTemplate', () => {
|
|
14
|
+
let tempDir: string;
|
|
15
|
+
const originalConsoleLog = console.log;
|
|
16
|
+
const originalConsoleError = console.error;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tempDir = path.join(process.cwd(), 'test-temp', `fetch-test-${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
|
20
|
+
fs.ensureDirSync(tempDir);
|
|
21
|
+
console.log = vi.fn();
|
|
22
|
+
console.error = vi.fn();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
if (fs.existsSync(tempDir)) {
|
|
27
|
+
fs.removeSync(tempDir);
|
|
28
|
+
}
|
|
29
|
+
console.log = originalConsoleLog;
|
|
30
|
+
console.error = originalConsoleError;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('Template fetching logic', () => {
|
|
34
|
+
it('should fetch and generate template successfully', async () => {
|
|
35
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
36
|
+
packageManager: 'npm',
|
|
37
|
+
projectName: 'test-project'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Verify template was generated
|
|
41
|
+
expect(fs.existsSync(path.join(tempDir, 'package.json'))).toBe(true);
|
|
42
|
+
expect(fs.existsSync(path.join(tempDir, 'src', 'index.ts'))).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should use default project name from path when not provided', async () => {
|
|
46
|
+
const customPath = path.join(tempDir, 'my-project');
|
|
47
|
+
await fetchCloudflareMcpiTemplate(customPath, {
|
|
48
|
+
packageManager: 'npm'
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const packageJsonPath = path.join(customPath, 'package.json');
|
|
52
|
+
const packageJson = fs.readJsonSync(packageJsonPath);
|
|
53
|
+
expect(packageJson.name).toBe('my-project');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle different package managers', async () => {
|
|
57
|
+
for (const pm of ['npm', 'yarn', 'pnpm']) {
|
|
58
|
+
const pmDir = path.join(tempDir, `test-${pm}`);
|
|
59
|
+
await fetchCloudflareMcpiTemplate(pmDir, {
|
|
60
|
+
packageManager: pm,
|
|
61
|
+
projectName: `test-${pm}`
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const packageJsonPath = path.join(pmDir, 'package.json');
|
|
65
|
+
expect(fs.existsSync(packageJsonPath)).toBe(true);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should generate template with correct project structure', async () => {
|
|
70
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
71
|
+
packageManager: 'npm',
|
|
72
|
+
projectName: 'test-project'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Check directory structure
|
|
76
|
+
expect(fs.existsSync(path.join(tempDir, 'src'))).toBe(true);
|
|
77
|
+
expect(fs.existsSync(path.join(tempDir, 'src', 'tools'))).toBe(true);
|
|
78
|
+
expect(fs.existsSync(path.join(tempDir, 'scripts'))).toBe(true);
|
|
79
|
+
expect(fs.existsSync(path.join(tempDir, 'tests'))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('Template caching', () => {
|
|
84
|
+
it('should generate consistent template structure on multiple calls', async () => {
|
|
85
|
+
const dir1 = path.join(tempDir, 'project1');
|
|
86
|
+
const dir2 = path.join(tempDir, 'project2');
|
|
87
|
+
|
|
88
|
+
await fetchCloudflareMcpiTemplate(dir1, {
|
|
89
|
+
packageManager: 'npm',
|
|
90
|
+
projectName: 'project1'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await fetchCloudflareMcpiTemplate(dir2, {
|
|
94
|
+
packageManager: 'npm',
|
|
95
|
+
projectName: 'project2'
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Both should have same structure
|
|
99
|
+
const files1 = fs.readdirSync(dir1);
|
|
100
|
+
const files2 = fs.readdirSync(dir2);
|
|
101
|
+
|
|
102
|
+
// Should have same top-level files
|
|
103
|
+
expect(files1.sort()).toEqual(files2.sort());
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle template generation without overwriting existing files incorrectly', async () => {
|
|
107
|
+
// Create existing file
|
|
108
|
+
const existingFile = path.join(tempDir, 'package.json');
|
|
109
|
+
fs.writeJsonSync(existingFile, { name: 'existing' });
|
|
110
|
+
|
|
111
|
+
// Generate template - should overwrite
|
|
112
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
113
|
+
packageManager: 'npm',
|
|
114
|
+
projectName: 'new-project'
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const packageJson = fs.readJsonSync(existingFile);
|
|
118
|
+
expect(packageJson.name).toBe('new-project');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Error handling for network failures', () => {
|
|
123
|
+
it('should handle missing dependencies gracefully', async () => {
|
|
124
|
+
// This test verifies that the function doesn't fail if dependencies are missing
|
|
125
|
+
// Since this is a local file operation, we test that it handles file system errors
|
|
126
|
+
const invalidPath = '/invalid/path/that/does/not/exist';
|
|
127
|
+
|
|
128
|
+
await expect(async () => {
|
|
129
|
+
await fetchCloudflareMcpiTemplate(invalidPath, {
|
|
130
|
+
packageManager: 'npm',
|
|
131
|
+
projectName: 'test'
|
|
132
|
+
});
|
|
133
|
+
}).rejects.toThrow();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle permission errors gracefully', async () => {
|
|
137
|
+
const readOnlyDir = path.join(tempDir, 'readonly');
|
|
138
|
+
fs.ensureDirSync(readOnlyDir);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Make directory read-only (if possible on this system)
|
|
142
|
+
fs.chmodSync(readOnlyDir, 0o444);
|
|
143
|
+
|
|
144
|
+
await expect(async () => {
|
|
145
|
+
await fetchCloudflareMcpiTemplate(readOnlyDir, {
|
|
146
|
+
packageManager: 'npm',
|
|
147
|
+
projectName: 'test'
|
|
148
|
+
});
|
|
149
|
+
}).rejects.toThrow();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Permission errors are expected
|
|
152
|
+
expect(error).toBeDefined();
|
|
153
|
+
} finally {
|
|
154
|
+
// Restore permissions for cleanup
|
|
155
|
+
try {
|
|
156
|
+
fs.chmodSync(readOnlyDir, 0o755);
|
|
157
|
+
} catch {
|
|
158
|
+
// Ignore cleanup errors
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should handle disk space errors (simulated)', async () => {
|
|
164
|
+
// Test that function handles file write errors
|
|
165
|
+
const diskFullDir = path.join(tempDir, 'disk-full');
|
|
166
|
+
|
|
167
|
+
// Mock fs.writeFileSync to throw error
|
|
168
|
+
const originalWriteFileSync = fs.writeFileSync;
|
|
169
|
+
const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementationOnce(() => {
|
|
170
|
+
throw new Error('ENOSPC: No space left on device');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await expect(async () => {
|
|
174
|
+
await fetchCloudflareMcpiTemplate(diskFullDir, {
|
|
175
|
+
packageManager: 'npm',
|
|
176
|
+
projectName: 'test'
|
|
177
|
+
});
|
|
178
|
+
}).rejects.toThrow();
|
|
179
|
+
|
|
180
|
+
writeFileSyncSpy.mockRestore();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Template transformation', () => {
|
|
185
|
+
it('should transform project name to valid class name', async () => {
|
|
186
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
187
|
+
packageManager: 'npm',
|
|
188
|
+
projectName: 'my-awesome-project'
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const indexPath = path.join(tempDir, 'src', 'index.ts');
|
|
192
|
+
const indexContent = fs.readFileSync(indexPath, 'utf-8');
|
|
193
|
+
|
|
194
|
+
// Should sanitize special characters - "my-awesome-project" becomes "MyawesomeprojectMCP"
|
|
195
|
+
// Class name should not contain hyphens
|
|
196
|
+
expect(indexContent).toMatch(/class\s+\w+MCP/);
|
|
197
|
+
// Should not contain the original hyphenated name in the class declaration
|
|
198
|
+
const classMatch = indexContent.match(/class\s+(\w+)MCP/);
|
|
199
|
+
expect(classMatch).toBeTruthy();
|
|
200
|
+
if (classMatch) {
|
|
201
|
+
expect(classMatch[1]).not.toContain('-');
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle special characters in project name', async () => {
|
|
206
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
207
|
+
packageManager: 'npm',
|
|
208
|
+
projectName: 'test@123!project'
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const indexPath = path.join(tempDir, 'src', 'index.ts');
|
|
212
|
+
const indexContent = fs.readFileSync(indexPath, 'utf-8');
|
|
213
|
+
|
|
214
|
+
// Should sanitize special characters
|
|
215
|
+
expect(indexContent).toMatch(/class\s+\w+MCP/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should handle numeric project names', async () => {
|
|
219
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
220
|
+
packageManager: 'npm',
|
|
221
|
+
projectName: '123project'
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const indexPath = path.join(tempDir, 'src', 'index.ts');
|
|
225
|
+
const indexContent = fs.readFileSync(indexPath, 'utf-8');
|
|
226
|
+
|
|
227
|
+
// Should prefix with underscore if starts with number
|
|
228
|
+
expect(indexContent).toMatch(/class\s+_?\w+MCP/);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should transform KV namespace bindings correctly', async () => {
|
|
232
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
233
|
+
packageManager: 'npm',
|
|
234
|
+
projectName: 'TestProject'
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const wranglerPath = path.join(tempDir, 'wrangler.toml');
|
|
238
|
+
const wranglerContent = fs.readFileSync(wranglerPath, 'utf-8');
|
|
239
|
+
|
|
240
|
+
// Should use uppercase class name for KV bindings
|
|
241
|
+
expect(wranglerContent).toContain('TESTPROJECT_');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should transform package.json scripts correctly', async () => {
|
|
245
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
246
|
+
packageManager: 'npm',
|
|
247
|
+
projectName: 'MyProject'
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const packageJsonPath = path.join(tempDir, 'package.json');
|
|
251
|
+
const packageJson = fs.readJsonSync(packageJsonPath);
|
|
252
|
+
|
|
253
|
+
// Scripts should use transformed class name
|
|
254
|
+
expect(packageJson.scripts['kv:create-nonce']).toContain('MYPROJECT_');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should transform index.ts class name correctly', async () => {
|
|
258
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
259
|
+
packageManager: 'npm',
|
|
260
|
+
projectName: 'test-project'
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const indexPath = path.join(tempDir, 'src', 'index.ts');
|
|
264
|
+
const indexContent = fs.readFileSync(indexPath, 'utf-8');
|
|
265
|
+
|
|
266
|
+
// Should create PascalCase class - "test-project" becomes "TestprojectMCP"
|
|
267
|
+
expect(indexContent).toContain('class TestprojectMCP');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('API key handling', () => {
|
|
272
|
+
it('should include API key in .dev.vars when provided', async () => {
|
|
273
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
274
|
+
packageManager: 'npm',
|
|
275
|
+
projectName: 'test-project',
|
|
276
|
+
apikey: 'sk_test_1234567890'
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const devVarsPath = path.join(tempDir, '.dev.vars');
|
|
280
|
+
if (fs.existsSync(devVarsPath)) {
|
|
281
|
+
const devVarsContent = fs.readFileSync(devVarsPath, 'utf-8');
|
|
282
|
+
expect(devVarsContent).toContain('sk_test_1234567890');
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should handle missing API key gracefully', async () => {
|
|
287
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
288
|
+
packageManager: 'npm',
|
|
289
|
+
projectName: 'test-project'
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Template should still be generated
|
|
293
|
+
expect(fs.existsSync(path.join(tempDir, 'package.json'))).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('Skip identity option', () => {
|
|
298
|
+
it('should skip identity generation when skipIdentity is true', async () => {
|
|
299
|
+
const generateIdentitySpy = vi.spyOn(await import('../../helpers/generate-identity'), 'generateIdentity');
|
|
300
|
+
|
|
301
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
302
|
+
packageManager: 'npm',
|
|
303
|
+
projectName: 'test-project',
|
|
304
|
+
skipIdentity: true
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// generateIdentity should not be called
|
|
308
|
+
expect(generateIdentitySpy).not.toHaveBeenCalled();
|
|
309
|
+
|
|
310
|
+
generateIdentitySpy.mockRestore();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should generate identity when skipIdentity is false', async () => {
|
|
314
|
+
const generateIdentitySpy = vi.spyOn(await import('../../helpers/generate-identity'), 'generateIdentity');
|
|
315
|
+
generateIdentitySpy.mockResolvedValue({
|
|
316
|
+
did: 'did:key:ztest',
|
|
317
|
+
kid: 'did:key:ztest#key-1',
|
|
318
|
+
privateKey: 'test-private',
|
|
319
|
+
publicKey: 'test-public',
|
|
320
|
+
createdAt: new Date().toISOString(),
|
|
321
|
+
type: 'development'
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await fetchCloudflareMcpiTemplate(tempDir, {
|
|
325
|
+
packageManager: 'npm',
|
|
326
|
+
projectName: 'test-project',
|
|
327
|
+
skipIdentity: false
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// generateIdentity should be called
|
|
331
|
+
expect(generateIdentitySpy).toHaveBeenCalled();
|
|
332
|
+
|
|
333
|
+
generateIdentitySpy.mockRestore();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Config Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for config generation from prompts, default values, config merging,
|
|
5
|
+
* and validation rules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
9
|
+
import { generateConfig } from '../../helpers/generate-config';
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
describe('generateConfig', () => {
|
|
14
|
+
let tempDir: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Ensure parent directory exists first to avoid race conditions
|
|
18
|
+
const testTempDir = path.join(process.cwd(), 'test-temp');
|
|
19
|
+
fs.ensureDirSync(testTempDir);
|
|
20
|
+
tempDir = path.join(testTempDir, `config-test-${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
|
21
|
+
fs.ensureDirSync(tempDir);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (fs.existsSync(tempDir)) {
|
|
26
|
+
fs.removeSync(tempDir);
|
|
27
|
+
}
|
|
28
|
+
if (fs.existsSync(path.join(process.cwd(), 'test-temp'))) {
|
|
29
|
+
const testTempContents = fs.readdirSync(path.join(process.cwd(), 'test-temp'));
|
|
30
|
+
if (testTempContents.length === 0) {
|
|
31
|
+
fs.removeSync(path.join(process.cwd(), 'test-temp'));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('Config generation from prompts', () => {
|
|
37
|
+
it('should generate xmcp.config.ts with default values', () => {
|
|
38
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
39
|
+
|
|
40
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
41
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
42
|
+
|
|
43
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
44
|
+
expect(configContent).toContain('XmcpConfig');
|
|
45
|
+
expect(configContent).toContain('paths');
|
|
46
|
+
expect(configContent).toContain('./src/tools');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should generate config with HTTP transport when specified', () => {
|
|
50
|
+
generateConfig(tempDir, ['http'], false);
|
|
51
|
+
|
|
52
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
53
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
54
|
+
|
|
55
|
+
expect(configContent).toContain('http: true');
|
|
56
|
+
expect(configContent).not.toContain('stdio: true');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should generate config with STDIO transport when specified', () => {
|
|
60
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
61
|
+
|
|
62
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
63
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
64
|
+
|
|
65
|
+
expect(configContent).toContain('stdio: true');
|
|
66
|
+
expect(configContent).not.toContain('http: true');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should generate config with both transports when both specified', () => {
|
|
70
|
+
generateConfig(tempDir, ['http', 'stdio'], false);
|
|
71
|
+
|
|
72
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
73
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
74
|
+
|
|
75
|
+
expect(configContent).toContain('http: true');
|
|
76
|
+
expect(configContent).toContain('stdio: true');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should generate config without identity when skipIdentity is true', () => {
|
|
80
|
+
generateConfig(tempDir, ['stdio'], true);
|
|
81
|
+
|
|
82
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
83
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
84
|
+
|
|
85
|
+
expect(configContent).not.toContain('identity:');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should generate config with identity when skipIdentity is false', () => {
|
|
89
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
90
|
+
|
|
91
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
92
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
93
|
+
|
|
94
|
+
expect(configContent).toContain('identity:');
|
|
95
|
+
expect(configContent).toContain('enabled: true');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('Default values', () => {
|
|
100
|
+
it('should use default tools path', () => {
|
|
101
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
102
|
+
|
|
103
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
104
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
105
|
+
|
|
106
|
+
expect(configContent).toContain('tools: "./src/tools"');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should use default development environment for identity', () => {
|
|
110
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
111
|
+
|
|
112
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
113
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
114
|
+
|
|
115
|
+
expect(configContent).toContain('environment: "development"');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should generate runtime config with default values', () => {
|
|
119
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
120
|
+
|
|
121
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
122
|
+
expect(fs.existsSync(runtimeConfigPath)).toBe(true);
|
|
123
|
+
|
|
124
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
125
|
+
expect(runtimeConfigContent).toContain('getRuntimeConfig');
|
|
126
|
+
expect(runtimeConfigContent).toContain('buildBaseConfig');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should include default Node.js server configuration', () => {
|
|
130
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
131
|
+
|
|
132
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
133
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
134
|
+
|
|
135
|
+
expect(runtimeConfigContent).toContain('port:');
|
|
136
|
+
expect(runtimeConfigContent).toContain('host:');
|
|
137
|
+
expect(runtimeConfigContent).toContain('cors:');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('Config merging', () => {
|
|
142
|
+
it('should merge base config with Node.js-specific properties', () => {
|
|
143
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
144
|
+
|
|
145
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
146
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
147
|
+
|
|
148
|
+
// Should include base config properties
|
|
149
|
+
expect(runtimeConfigContent).toContain('baseConfig');
|
|
150
|
+
// Should include Node.js-specific properties
|
|
151
|
+
expect(runtimeConfigContent).toContain('server:');
|
|
152
|
+
expect(runtimeConfigContent).toContain('storage:');
|
|
153
|
+
expect(runtimeConfigContent).toContain('nodeEnv:');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should merge identity config when not skipped', () => {
|
|
157
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
158
|
+
|
|
159
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
160
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
161
|
+
|
|
162
|
+
// The runtime config uses buildBaseConfig which includes identity
|
|
163
|
+
// Check that buildBaseConfig is called (which includes identity)
|
|
164
|
+
expect(runtimeConfigContent).toContain('buildBaseConfig');
|
|
165
|
+
expect(runtimeConfigContent).toContain('baseConfig');
|
|
166
|
+
|
|
167
|
+
// Also verify that xmcp.config.ts contains identity when not skipped
|
|
168
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
169
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
170
|
+
expect(configContent).toContain('identity');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should merge proofing config', () => {
|
|
174
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
175
|
+
|
|
176
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
177
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
178
|
+
|
|
179
|
+
// The runtime config uses buildBaseConfig which includes proofing
|
|
180
|
+
// buildBaseConfig is imported and called, which internally includes proofing configuration
|
|
181
|
+
expect(runtimeConfigContent).toContain('buildBaseConfig');
|
|
182
|
+
expect(runtimeConfigContent).toContain('baseConfig');
|
|
183
|
+
// The baseConfig spread includes proofing via buildBaseConfig
|
|
184
|
+
expect(runtimeConfigContent).toContain('...baseConfig');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should merge delegation config', () => {
|
|
188
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
189
|
+
|
|
190
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
191
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
192
|
+
|
|
193
|
+
expect(runtimeConfigContent).toContain('delegation');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('Validation rules', () => {
|
|
198
|
+
it('should create valid TypeScript config file', () => {
|
|
199
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
200
|
+
|
|
201
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
202
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
203
|
+
|
|
204
|
+
// Should be valid TypeScript syntax
|
|
205
|
+
expect(configContent).toContain('import');
|
|
206
|
+
expect(configContent).toContain('export default');
|
|
207
|
+
expect(configContent).toContain('const config');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should create valid TypeScript runtime config file', () => {
|
|
211
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
212
|
+
|
|
213
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
214
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
215
|
+
|
|
216
|
+
// Should be valid TypeScript syntax
|
|
217
|
+
expect(runtimeConfigContent).toContain('import');
|
|
218
|
+
expect(runtimeConfigContent).toContain('export function');
|
|
219
|
+
expect(runtimeConfigContent).toContain('NodeRuntimeConfig');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should validate transport options', () => {
|
|
223
|
+
// Empty transports array should still work
|
|
224
|
+
generateConfig(tempDir, [], false);
|
|
225
|
+
|
|
226
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
227
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should handle invalid transport values gracefully', () => {
|
|
231
|
+
// Invalid transports should be ignored
|
|
232
|
+
generateConfig(tempDir, ['invalid-transport' as any], false);
|
|
233
|
+
|
|
234
|
+
const configPath = path.join(tempDir, 'xmcp.config.ts');
|
|
235
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
236
|
+
|
|
237
|
+
// Should not contain invalid transport
|
|
238
|
+
expect(configContent).not.toContain('invalid-transport');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should validate file paths exist', () => {
|
|
242
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
243
|
+
|
|
244
|
+
// Should create src directory for runtime config
|
|
245
|
+
expect(fs.existsSync(path.join(tempDir, 'src'))).toBe(true);
|
|
246
|
+
expect(fs.existsSync(path.join(tempDir, 'src', 'mcpi-runtime-config.ts'))).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('Environment variable handling', () => {
|
|
251
|
+
it('should use process.env in runtime config', () => {
|
|
252
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
253
|
+
|
|
254
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
255
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
256
|
+
|
|
257
|
+
expect(runtimeConfigContent).toContain('process.env');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle PORT environment variable', () => {
|
|
261
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
262
|
+
|
|
263
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
264
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
265
|
+
|
|
266
|
+
expect(runtimeConfigContent).toContain('PORT');
|
|
267
|
+
expect(runtimeConfigContent).toContain('"3000"');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should handle HOST environment variable', () => {
|
|
271
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
272
|
+
|
|
273
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
274
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
275
|
+
|
|
276
|
+
expect(runtimeConfigContent).toContain('HOST');
|
|
277
|
+
expect(runtimeConfigContent).toContain('"0.0.0.0"');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should handle NODE_ENV environment variable', () => {
|
|
281
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
282
|
+
|
|
283
|
+
const runtimeConfigPath = path.join(tempDir, 'src', 'mcpi-runtime-config.ts');
|
|
284
|
+
const runtimeConfigContent = fs.readFileSync(runtimeConfigPath, 'utf-8');
|
|
285
|
+
|
|
286
|
+
expect(runtimeConfigContent).toContain('NODE_ENV');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('File structure', () => {
|
|
291
|
+
it('should create src directory if it does not exist', () => {
|
|
292
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
293
|
+
|
|
294
|
+
expect(fs.existsSync(path.join(tempDir, 'src'))).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should not overwrite existing src directory', () => {
|
|
298
|
+
const srcDir = path.join(tempDir, 'src');
|
|
299
|
+
fs.ensureDirSync(srcDir);
|
|
300
|
+
const existingFile = path.join(srcDir, 'existing.ts');
|
|
301
|
+
fs.writeFileSync(existingFile, '// existing file');
|
|
302
|
+
|
|
303
|
+
generateConfig(tempDir, ['stdio'], false);
|
|
304
|
+
|
|
305
|
+
// Existing file should still exist
|
|
306
|
+
expect(fs.existsSync(existingFile)).toBe(true);
|
|
307
|
+
// Runtime config should also exist
|
|
308
|
+
expect(fs.existsSync(path.join(srcDir, 'mcpi-runtime-config.ts'))).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|