@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.
Files changed (98) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test$colon$coverage.log +315 -0
  3. package/.turbo/turbo-test.log +95 -0
  4. package/CHANGELOG.md +372 -0
  5. package/IMPLEMENTATION_SUMMARY.md +108 -0
  6. package/REMEDIATION_PLAN.md +99 -0
  7. package/coverage/base.css +224 -0
  8. package/coverage/block-navigation.js +87 -0
  9. package/coverage/clover.xml +252 -0
  10. package/coverage/config-builder.ts.html +580 -0
  11. package/coverage/coverage-final.json +7 -0
  12. package/coverage/favicon.png +0 -0
  13. package/coverage/fetch-cloudflare-mcpi-template.ts.html +7006 -0
  14. package/coverage/generate-config.ts.html +436 -0
  15. package/coverage/generate-identity.ts.html +574 -0
  16. package/coverage/index.html +191 -0
  17. package/coverage/install.ts.html +322 -0
  18. package/coverage/prettify.css +1 -0
  19. package/coverage/prettify.js +2 -0
  20. package/coverage/sort-arrow-sprite.png +0 -0
  21. package/coverage/sorter.js +210 -0
  22. package/coverage/validate-project-structure.ts.html +466 -0
  23. package/dist/.tsbuildinfo +1 -1
  24. package/dist/helpers/__tests__/config-builder.spec.d.ts +8 -0
  25. package/dist/helpers/__tests__/config-builder.spec.d.ts.map +1 -0
  26. package/dist/helpers/__tests__/config-builder.spec.js +182 -0
  27. package/dist/helpers/__tests__/config-builder.spec.js.map +1 -0
  28. package/dist/helpers/config-builder.d.ts +58 -0
  29. package/dist/helpers/config-builder.d.ts.map +1 -0
  30. package/dist/helpers/config-builder.js +102 -0
  31. package/dist/helpers/config-builder.js.map +1 -0
  32. package/dist/helpers/create.d.ts +1 -0
  33. package/dist/helpers/create.d.ts.map +1 -1
  34. package/dist/helpers/create.js +2 -1
  35. package/dist/helpers/create.js.map +1 -1
  36. package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts +1 -0
  37. package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts.map +1 -1
  38. package/dist/helpers/fetch-cloudflare-mcpi-template.js +209 -174
  39. package/dist/helpers/fetch-cloudflare-mcpi-template.js.map +1 -1
  40. package/dist/helpers/fetch-mcpi-template.d.ts.map +1 -1
  41. package/dist/helpers/fetch-mcpi-template.js +18 -3
  42. package/dist/helpers/fetch-mcpi-template.js.map +1 -1
  43. package/dist/helpers/generate-config.d.ts.map +1 -1
  44. package/dist/helpers/generate-config.js +27 -40
  45. package/dist/helpers/generate-config.js.map +1 -1
  46. package/dist/helpers/install.js +5 -0
  47. package/dist/helpers/install.js.map +1 -1
  48. package/dist/index.js +2 -0
  49. package/dist/index.js.map +1 -1
  50. package/package.json +18 -9
  51. package/scripts/prepare-pack.js +47 -0
  52. package/scripts/validate-no-workspace.js +79 -0
  53. package/src/__tests__/cloudflare-template.test.ts +488 -0
  54. package/src/__tests__/helpers/fetch-cloudflare-mcpi-template.test.ts +337 -0
  55. package/src/__tests__/helpers/generate-config.test.ts +312 -0
  56. package/src/__tests__/helpers/generate-identity.test.ts +271 -0
  57. package/src/__tests__/helpers/install.test.ts +362 -0
  58. package/src/__tests__/helpers/validate-project-structure.test.ts +467 -0
  59. package/src/__tests__.bak/regression.test.ts +434 -0
  60. package/src/effects/index.ts +80 -0
  61. package/src/helpers/__tests__/config-builder.spec.ts +231 -0
  62. package/src/helpers/apply-identity-preset.ts +209 -0
  63. package/src/helpers/config-builder.ts +165 -0
  64. package/src/helpers/copy-template.ts +11 -0
  65. package/src/helpers/create.ts +239 -0
  66. package/src/helpers/fetch-cloudflare-mcpi-template.ts +2311 -0
  67. package/src/helpers/fetch-cloudflare-template.ts +361 -0
  68. package/src/helpers/fetch-mcpi-template.ts +236 -0
  69. package/src/helpers/fetch-xmcp-template.ts +153 -0
  70. package/src/helpers/generate-config.ts +117 -0
  71. package/src/helpers/generate-identity.ts +163 -0
  72. package/src/helpers/identity-manager.ts +186 -0
  73. package/src/helpers/install.ts +79 -0
  74. package/src/helpers/rename.ts +17 -0
  75. package/src/helpers/validate-project-structure.ts +127 -0
  76. package/src/index.ts +480 -0
  77. package/src/utils/check-node.ts +17 -0
  78. package/src/utils/is-folder-empty.ts +60 -0
  79. package/src/utils/validate-project-name.ts +132 -0
  80. package/test-cloudflare/README.md +164 -0
  81. package/test-cloudflare/package.json +28 -0
  82. package/test-cloudflare/src/index.ts +340 -0
  83. package/test-cloudflare/src/tools/greet.ts +19 -0
  84. package/test-cloudflare/tests/cache-invalidation.test.ts +410 -0
  85. package/test-cloudflare/tests/cors-security.test.ts +349 -0
  86. package/test-cloudflare/tests/delegation.test.ts +335 -0
  87. package/test-cloudflare/tests/do-routing.test.ts +314 -0
  88. package/test-cloudflare/tests/integration.test.ts +205 -0
  89. package/test-cloudflare/tests/session-management.test.ts +359 -0
  90. package/test-cloudflare/tsconfig.json +22 -0
  91. package/test-cloudflare/vitest.config.ts +9 -0
  92. package/test-cloudflare/wrangler.toml +37 -0
  93. package/test-node/README.md +44 -0
  94. package/test-node/package.json +23 -0
  95. package/test-node/src/tools/greet.ts +25 -0
  96. package/test-node/xmcp.config.ts +20 -0
  97. package/tsconfig.json +26 -0
  98. package/vitest.config.ts +14 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Generate Identity Tests
3
+ *
4
+ * Tests for identity generation, key pair creation, DID format validation,
5
+ * and identity persistence.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
9
+ import {
10
+ generateIdentity,
11
+ isValidDIDKey,
12
+ extractMultibaseFromDID,
13
+ type GeneratedIdentity
14
+ } from '../../helpers/generate-identity';
15
+
16
+ describe('generateIdentity', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ describe('Identity generation', () => {
22
+ it('should generate a valid identity', async () => {
23
+ const identity = await generateIdentity();
24
+
25
+ expect(identity).toBeDefined();
26
+ expect(identity.did).toBeDefined();
27
+ expect(identity.kid).toBeDefined();
28
+ expect(identity.privateKey).toBeDefined();
29
+ expect(identity.publicKey).toBeDefined();
30
+ expect(identity.createdAt).toBeDefined();
31
+ expect(identity.type).toBe('development');
32
+ });
33
+
34
+ it('should generate unique identities on each call', async () => {
35
+ const identity1 = await generateIdentity();
36
+ const identity2 = await generateIdentity();
37
+
38
+ expect(identity1.did).not.toBe(identity2.did);
39
+ expect(identity1.privateKey).not.toBe(identity2.privateKey);
40
+ expect(identity1.publicKey).not.toBe(identity2.publicKey);
41
+ });
42
+
43
+ it('should generate identity with correct structure', async () => {
44
+ const identity = await generateIdentity();
45
+
46
+ expect(typeof identity.did).toBe('string');
47
+ expect(typeof identity.kid).toBe('string');
48
+ expect(typeof identity.privateKey).toBe('string');
49
+ expect(typeof identity.publicKey).toBe('string');
50
+ expect(typeof identity.createdAt).toBe('string');
51
+ expect(typeof identity.type).toBe('string');
52
+ });
53
+
54
+ it('should generate identity with valid ISO timestamp', async () => {
55
+ const identity = await generateIdentity();
56
+ const timestamp = new Date(identity.createdAt);
57
+
58
+ expect(timestamp.getTime()).not.toBeNaN();
59
+ expect(timestamp.toISOString()).toBe(identity.createdAt);
60
+ });
61
+ });
62
+
63
+ describe('Key pair creation', () => {
64
+ it('should generate valid Ed25519 key pair', async () => {
65
+ const identity = await generateIdentity();
66
+
67
+ // Private key should be base64 encoded (32 bytes raw = 44 chars base64)
68
+ const privateKeyBuffer = Buffer.from(identity.privateKey, 'base64');
69
+ expect(privateKeyBuffer.length).toBe(32);
70
+
71
+ // Public key should be base64 encoded (32 bytes raw = 44 chars base64)
72
+ const publicKeyBuffer = Buffer.from(identity.publicKey, 'base64');
73
+ expect(publicKeyBuffer.length).toBe(32);
74
+ });
75
+
76
+ it('should generate keys in correct format', async () => {
77
+ const identity = await generateIdentity();
78
+
79
+ // Keys should be valid base64 strings
80
+ expect(/^[A-Za-z0-9+/=]+$/.test(identity.privateKey)).toBe(true);
81
+ expect(/^[A-Za-z0-9+/=]+$/.test(identity.publicKey)).toBe(true);
82
+ });
83
+
84
+ it('should generate different private and public keys', async () => {
85
+ const identity = await generateIdentity();
86
+
87
+ expect(identity.privateKey).not.toBe(identity.publicKey);
88
+ });
89
+
90
+ it('should generate keys that can be used for signing', async () => {
91
+ const identity = await generateIdentity();
92
+
93
+ // Keys should be valid base64 and decode to 32 bytes
94
+ const privateKey = Buffer.from(identity.privateKey, 'base64');
95
+ const publicKey = Buffer.from(identity.publicKey, 'base64');
96
+
97
+ expect(privateKey.length).toBe(32);
98
+ expect(publicKey.length).toBe(32);
99
+ });
100
+ });
101
+
102
+ describe('DID format validation', () => {
103
+ it('should generate valid DID:key format', async () => {
104
+ const identity = await generateIdentity();
105
+
106
+ expect(isValidDIDKey(identity.did)).toBe(true);
107
+ expect(identity.did).toMatch(/^did:key:z/);
108
+ });
109
+
110
+ it('should generate DID with correct prefix', async () => {
111
+ const identity = await generateIdentity();
112
+
113
+ expect(identity.did.startsWith('did:key:z')).toBe(true);
114
+ });
115
+
116
+ it('should generate KID that references DID', async () => {
117
+ const identity = await generateIdentity();
118
+
119
+ expect(identity.kid).toBe(`${identity.did}#key-1`);
120
+ });
121
+
122
+ it('should validate correctly formatted DID', () => {
123
+ expect(isValidDIDKey('did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK')).toBe(true);
124
+ // Minimum valid length - needs at least 47 characters after 'z' according to regex
125
+ // A shorter DID like 'did:key:z6Mk' is not valid
126
+ expect(isValidDIDKey('did:key:z6Mk')).toBe(false);
127
+ });
128
+
129
+ it('should reject invalid DID formats', () => {
130
+ expect(isValidDIDKey('did:key:')).toBe(false);
131
+ expect(isValidDIDKey('did:key:z')).toBe(false);
132
+ expect(isValidDIDKey('did:web:example.com')).toBe(false);
133
+ expect(isValidDIDKey('not-a-did')).toBe(false);
134
+ expect(isValidDIDKey('')).toBe(false);
135
+ });
136
+
137
+ it('should extract multibase from valid DID', () => {
138
+ const did = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK';
139
+ const multibase = extractMultibaseFromDID(did);
140
+
141
+ expect(multibase).toBe('6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK');
142
+ expect(multibase).not.toContain('did:key:');
143
+ // Multibase should not contain 'z' prefix (it's removed)
144
+ expect(multibase).toBeTruthy();
145
+ expect(multibase?.charAt(0)).not.toBe('z');
146
+ });
147
+
148
+ it('should return null for invalid DID when extracting multibase', () => {
149
+ expect(extractMultibaseFromDID('invalid-did')).toBeNull();
150
+ expect(extractMultibaseFromDID('did:key:')).toBeNull();
151
+ expect(extractMultibaseFromDID('')).toBeNull();
152
+ });
153
+ });
154
+
155
+ describe('Identity persistence', () => {
156
+ it('should generate identity that can be serialized to JSON', async () => {
157
+ const identity = await generateIdentity();
158
+
159
+ const json = JSON.stringify(identity);
160
+ expect(json).toBeDefined();
161
+
162
+ const parsed = JSON.parse(json) as GeneratedIdentity;
163
+ expect(parsed.did).toBe(identity.did);
164
+ expect(parsed.kid).toBe(identity.kid);
165
+ expect(parsed.privateKey).toBe(identity.privateKey);
166
+ expect(parsed.publicKey).toBe(identity.publicKey);
167
+ expect(parsed.createdAt).toBe(identity.createdAt);
168
+ expect(parsed.type).toBe(identity.type);
169
+ });
170
+
171
+ it('should generate identity with consistent structure', async () => {
172
+ const identities = await Promise.all([
173
+ generateIdentity(),
174
+ generateIdentity(),
175
+ generateIdentity()
176
+ ]);
177
+
178
+ // All identities should have same structure
179
+ identities.forEach(identity => {
180
+ expect(identity).toHaveProperty('did');
181
+ expect(identity).toHaveProperty('kid');
182
+ expect(identity).toHaveProperty('privateKey');
183
+ expect(identity).toHaveProperty('publicKey');
184
+ expect(identity).toHaveProperty('createdAt');
185
+ expect(identity).toHaveProperty('type');
186
+ });
187
+ });
188
+
189
+ it('should generate identity that matches expected interface', async () => {
190
+ const identity = await generateIdentity();
191
+
192
+ // Verify all required fields exist
193
+ const requiredFields: (keyof GeneratedIdentity)[] = [
194
+ 'did',
195
+ 'kid',
196
+ 'privateKey',
197
+ 'publicKey',
198
+ 'createdAt',
199
+ 'type'
200
+ ];
201
+
202
+ requiredFields.forEach(field => {
203
+ expect(identity).toHaveProperty(field);
204
+ expect(identity[field]).toBeDefined();
205
+ });
206
+ });
207
+
208
+ it('should generate identity with correct type', async () => {
209
+ const identity = await generateIdentity();
210
+
211
+ expect(identity.type).toBe('development');
212
+ expect(['development', 'production']).toContain(identity.type);
213
+ });
214
+ });
215
+
216
+ describe('DID generation from public key', () => {
217
+ it('should generate DID that corresponds to public key', async () => {
218
+ const identity1 = await generateIdentity();
219
+ const identity2 = await generateIdentity();
220
+
221
+ // Different public keys should generate different DIDs
222
+ expect(identity1.publicKey).not.toBe(identity2.publicKey);
223
+ expect(identity1.did).not.toBe(identity2.did);
224
+ });
225
+
226
+ it('should generate DID with correct length', async () => {
227
+ const identity = await generateIdentity();
228
+
229
+ // DID:key format should be: did:key:z + base58-encoded key (typically 47+ chars)
230
+ expect(identity.did.length).toBeGreaterThan(50);
231
+ expect(identity.did.length).toBeLessThan(100);
232
+ });
233
+
234
+ it('should generate DID with base58 encoding', async () => {
235
+ const identity = await generateIdentity();
236
+ const multibase = extractMultibaseFromDID(identity.did);
237
+
238
+ if (multibase) {
239
+ // Base58 should only contain valid characters (no 0, O, I, l)
240
+ expect(/^[1-9A-HJ-NP-Za-km-z]+$/.test(multibase)).toBe(true);
241
+ }
242
+ });
243
+ });
244
+
245
+ describe('Edge cases', () => {
246
+ it('should handle concurrent identity generation', async () => {
247
+ const promises = Array.from({ length: 10 }, () => generateIdentity());
248
+ const identities = await Promise.all(promises);
249
+
250
+ // All should be unique
251
+ const dids = identities.map(i => i.did);
252
+ const uniqueDids = new Set(dids);
253
+ expect(uniqueDids.size).toBe(10);
254
+ });
255
+
256
+ it('should generate identity consistently', async () => {
257
+ // Generate multiple identities and verify they're all valid
258
+ const identities = await Promise.all([
259
+ generateIdentity(),
260
+ generateIdentity(),
261
+ generateIdentity()
262
+ ]);
263
+
264
+ identities.forEach(identity => {
265
+ expect(isValidDIDKey(identity.did)).toBe(true);
266
+ expect(identity.kid).toBe(`${identity.did}#key-1`);
267
+ });
268
+ });
269
+ });
270
+ });
271
+
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Install Tests
3
+ *
4
+ * Tests for dependency installation, package manager detection,
5
+ * error handling, and install progress reporting.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
9
+ import fs from 'fs-extra';
10
+ import path from 'path';
11
+
12
+ // Mock child_process module
13
+ // Use a shared object to store the mock function
14
+ const mockExecSyncStore = { fn: vi.fn() };
15
+ vi.mock('child_process', () => ({
16
+ get execSync() {
17
+ return mockExecSyncStore.fn;
18
+ },
19
+ }));
20
+
21
+ // Import after mock setup
22
+ import { install, detectPackageManager } from '../../helpers/install';
23
+
24
+ // Export the mock for use in tests
25
+ const mockExecSync = mockExecSyncStore.fn;
26
+
27
+ describe('detectPackageManager', () => {
28
+ let tempDir: string;
29
+
30
+ beforeEach(() => {
31
+ tempDir = path.join(process.cwd(), 'test-temp', `detect-test-${Date.now()}-${Math.random().toString(36).substring(7)}`);
32
+ fs.ensureDirSync(tempDir);
33
+ });
34
+
35
+ afterEach(() => {
36
+ if (fs.existsSync(tempDir)) {
37
+ fs.removeSync(tempDir);
38
+ }
39
+ if (fs.existsSync(path.join(process.cwd(), 'test-temp'))) {
40
+ const testTempContents = fs.readdirSync(path.join(process.cwd(), 'test-temp'));
41
+ if (testTempContents.length === 0) {
42
+ fs.removeSync(path.join(process.cwd(), 'test-temp'));
43
+ }
44
+ }
45
+ });
46
+
47
+ describe('Package manager detection', () => {
48
+ it('should detect npm from package-lock.json', () => {
49
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
50
+
51
+ const result = detectPackageManager(tempDir);
52
+
53
+ expect(result).toBe('npm');
54
+ });
55
+
56
+ it('should detect yarn from yarn.lock', () => {
57
+ fs.writeFileSync(path.join(tempDir, 'yarn.lock'), '# yarn lock');
58
+
59
+ const result = detectPackageManager(tempDir);
60
+
61
+ expect(result).toBe('yarn');
62
+ });
63
+
64
+ it('should detect pnpm from pnpm-lock.yaml', () => {
65
+ fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), '# pnpm lock');
66
+
67
+ const result = detectPackageManager(tempDir);
68
+
69
+ expect(result).toBe('pnpm');
70
+ });
71
+
72
+ it('should return null when no lockfile exists', () => {
73
+ const result = detectPackageManager(tempDir);
74
+
75
+ expect(result).toBeNull();
76
+ });
77
+
78
+ it('should prioritize pnpm over npm', () => {
79
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
80
+ fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), '# pnpm lock');
81
+
82
+ const result = detectPackageManager(tempDir);
83
+
84
+ // pnpm should be detected first (check order in implementation)
85
+ expect(['pnpm', 'npm']).toContain(result);
86
+ });
87
+
88
+ it('should prioritize yarn over npm', () => {
89
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
90
+ fs.writeFileSync(path.join(tempDir, 'yarn.lock'), '# yarn lock');
91
+
92
+ const result = detectPackageManager(tempDir);
93
+
94
+ // yarn should be detected first (check order in implementation)
95
+ expect(['yarn', 'npm']).toContain(result);
96
+ });
97
+ });
98
+ });
99
+
100
+ describe('install', () => {
101
+ let tempDir: string;
102
+
103
+ beforeEach(() => {
104
+ tempDir = path.join(process.cwd(), 'test-temp', `install-test-${Date.now()}-${Math.random().toString(36).substring(7)}`);
105
+ fs.ensureDirSync(tempDir);
106
+
107
+ // Create minimal package.json for installation
108
+ fs.writeJsonSync(path.join(tempDir, 'package.json'), {
109
+ name: 'test-project',
110
+ version: '1.0.0',
111
+ dependencies: {}
112
+ });
113
+
114
+ // Reset mock before each test
115
+ mockExecSync.mockReset();
116
+ });
117
+
118
+ afterEach(() => {
119
+ if (fs.existsSync(tempDir)) {
120
+ fs.removeSync(tempDir);
121
+ }
122
+ if (fs.existsSync(path.join(process.cwd(), 'test-temp'))) {
123
+ const testTempContents = fs.readdirSync(path.join(process.cwd(), 'test-temp'));
124
+ if (testTempContents.length === 0) {
125
+ fs.removeSync(path.join(process.cwd(), 'test-temp'));
126
+ }
127
+ }
128
+ vi.clearAllMocks();
129
+ });
130
+
131
+ describe('Dependency installation', () => {
132
+ it('should install dependencies with npm', () => {
133
+ mockExecSync.mockImplementation(() => {
134
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
135
+ return Buffer.from('');
136
+ });
137
+
138
+ install(tempDir, 'npm', '1.0.0');
139
+
140
+ expect(mockExecSync).toHaveBeenCalledWith(
141
+ 'npm install',
142
+ expect.objectContaining({
143
+ cwd: tempDir,
144
+ stdio: 'inherit'
145
+ })
146
+ );
147
+ });
148
+
149
+ it('should install dependencies with yarn', () => {
150
+ mockExecSync.mockImplementation(() => {
151
+ fs.writeFileSync(path.join(tempDir, 'yarn.lock'), '# yarn lock');
152
+ return Buffer.from('');
153
+ });
154
+
155
+ install(tempDir, 'yarn', '1.0.0');
156
+
157
+ expect(mockExecSync).toHaveBeenCalledWith(
158
+ 'yarn install',
159
+ expect.objectContaining({
160
+ cwd: tempDir,
161
+ stdio: 'inherit'
162
+ })
163
+ );
164
+ });
165
+
166
+ it('should install dependencies with pnpm', () => {
167
+ mockExecSync.mockImplementation(() => {
168
+ fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), '# pnpm lock');
169
+ return Buffer.from('');
170
+ });
171
+
172
+ install(tempDir, 'pnpm', '1.0.0');
173
+
174
+ expect(mockExecSync).toHaveBeenCalledWith(
175
+ 'pnpm install',
176
+ expect.objectContaining({
177
+ cwd: tempDir,
178
+ stdio: 'inherit'
179
+ })
180
+ );
181
+ });
182
+
183
+ it('should use correct working directory', () => {
184
+ mockExecSync.mockImplementation(() => {
185
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
186
+ return Buffer.from('');
187
+ });
188
+
189
+ install(tempDir, 'npm', '1.0.0');
190
+
191
+ expect(mockExecSync).toHaveBeenCalledWith(
192
+ expect.any(String),
193
+ expect.objectContaining({
194
+ cwd: tempDir
195
+ })
196
+ );
197
+ });
198
+ });
199
+
200
+ describe('Install progress reporting', () => {
201
+ it('should log installation start message', () => {
202
+ mockExecSync.mockImplementation(() => {
203
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
204
+ return Buffer.from('');
205
+ });
206
+
207
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
208
+
209
+ install(tempDir, 'npm', '1.0.0');
210
+
211
+ expect(consoleLogSpy).toHaveBeenCalledWith(
212
+ expect.stringContaining('Installing dependencies')
213
+ );
214
+
215
+ consoleLogSpy.mockRestore();
216
+ });
217
+
218
+ it('should report correct package manager in log', () => {
219
+ mockExecSync.mockImplementation(() => {
220
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
221
+ return Buffer.from('');
222
+ });
223
+
224
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
225
+
226
+ install(tempDir, 'pnpm', '1.0.0');
227
+
228
+ expect(consoleLogSpy).toHaveBeenCalledWith(
229
+ expect.stringContaining('pnpm')
230
+ );
231
+
232
+ consoleLogSpy.mockRestore();
233
+ });
234
+
235
+ it('should check for lockfile after installation', () => {
236
+ mockExecSync.mockImplementation(() => {
237
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
238
+ return Buffer.from('');
239
+ });
240
+
241
+ install(tempDir, 'npm', '1.0.0');
242
+
243
+ // Should check for lockfile (via checkLockfile function)
244
+ expect(fs.existsSync(path.join(tempDir, 'package-lock.json'))).toBe(true);
245
+ });
246
+ });
247
+
248
+ describe('Error handling', () => {
249
+ it('should throw error when installation fails', () => {
250
+ mockExecSync.mockImplementation(() => {
251
+ throw new Error('Installation failed');
252
+ });
253
+
254
+ expect(() => {
255
+ install(tempDir, 'npm', '1.0.0');
256
+ }).toThrow('Installation failed');
257
+ });
258
+
259
+ it('should handle network errors during installation', () => {
260
+ mockExecSync.mockImplementation(() => {
261
+ throw new Error('ENOTFOUND registry.npmjs.org');
262
+ });
263
+
264
+ expect(() => {
265
+ install(tempDir, 'npm', '1.0.0');
266
+ }).toThrow();
267
+ });
268
+
269
+ it('should handle permission errors during installation', () => {
270
+ mockExecSync.mockImplementation(() => {
271
+ throw new Error('EACCES: permission denied');
272
+ });
273
+
274
+ expect(() => {
275
+ install(tempDir, 'npm', '1.0.0');
276
+ }).toThrow();
277
+ });
278
+
279
+ it('should log error message when installation fails', () => {
280
+ mockExecSync.mockImplementation(() => {
281
+ throw new Error('Installation failed');
282
+ });
283
+
284
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
285
+
286
+ try {
287
+ install(tempDir, 'npm', '1.0.0');
288
+ } catch {
289
+ // Expected to throw
290
+ }
291
+
292
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
293
+ expect.stringContaining('Failed to install dependencies')
294
+ );
295
+
296
+ consoleErrorSpy.mockRestore();
297
+ });
298
+
299
+ it('should handle invalid package manager gracefully', () => {
300
+ mockExecSync.mockImplementation(() => {
301
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
302
+ return Buffer.from('');
303
+ });
304
+
305
+ // Should default to npm for unknown package manager
306
+ install(tempDir, 'unknown' as any, '1.0.0');
307
+
308
+ expect(mockExecSync).toHaveBeenCalledWith(
309
+ 'npm install',
310
+ expect.any(Object)
311
+ );
312
+ });
313
+ });
314
+
315
+ describe('Lockfile validation', () => {
316
+ it('should validate npm lockfile after installation', () => {
317
+ mockExecSync.mockImplementation(() => {
318
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}');
319
+ return Buffer.from('');
320
+ });
321
+
322
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
323
+
324
+ install(tempDir, 'npm', '1.0.0');
325
+
326
+ // Should log lockfile creation message
327
+ // chalk.gray() wraps the message, so check that log was called
328
+ expect(consoleLogSpy).toHaveBeenCalled();
329
+ const logCalls = consoleLogSpy.mock.calls;
330
+ const hasLockfileMessage = logCalls.some(call =>
331
+ call[0] && typeof call[0] === 'string' &&
332
+ (call[0].includes('Lockfile created') || call[0].includes('package-lock.json'))
333
+ );
334
+ expect(hasLockfileMessage).toBe(true);
335
+
336
+ consoleLogSpy.mockRestore();
337
+ });
338
+
339
+ it('should warn when lockfile not created', () => {
340
+ mockExecSync.mockImplementation(() => {
341
+ // Mock successful install but don't create lockfile
342
+ // The checkLockfile function will check if file exists after install
343
+ return Buffer.from('');
344
+ });
345
+
346
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
347
+
348
+ install(tempDir, 'npm', '1.0.0');
349
+
350
+ // checkLockfile is called after install succeeds, and should warn if lockfile doesn't exist
351
+ // chalk.yellow() wraps the message, so we check that warn was called with a string containing the message
352
+ expect(consoleWarnSpy).toHaveBeenCalled();
353
+ const warnCalls = consoleWarnSpy.mock.calls;
354
+ const hasWarningMessage = warnCalls.some(call =>
355
+ call[0] && typeof call[0] === 'string' && call[0].includes('Warning: No lockfile generated')
356
+ );
357
+ expect(hasWarningMessage).toBe(true);
358
+
359
+ consoleWarnSpy.mockRestore();
360
+ });
361
+ });
362
+ });