@friggframework/devtools 2.0.0-next.41 → 2.0.0-next.43

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 (34) hide show
  1. package/frigg-cli/__tests__/unit/commands/build.test.js +173 -405
  2. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
  3. package/frigg-cli/__tests__/unit/commands/install.test.js +359 -377
  4. package/frigg-cli/__tests__/unit/commands/ui.test.js +266 -512
  5. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
  6. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
  7. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +486 -0
  8. package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
  9. package/frigg-cli/__tests__/utils/test-setup.js +22 -21
  10. package/frigg-cli/db-setup-command/index.js +186 -0
  11. package/frigg-cli/generate-command/__tests__/generate-command.test.js +151 -162
  12. package/frigg-cli/generate-iam-command.js +7 -4
  13. package/frigg-cli/index.js +9 -1
  14. package/frigg-cli/install-command/index.js +1 -1
  15. package/frigg-cli/jest.config.js +124 -0
  16. package/frigg-cli/package.json +4 -1
  17. package/frigg-cli/start-command/index.js +95 -2
  18. package/frigg-cli/start-command/start-command.test.js +161 -19
  19. package/frigg-cli/utils/database-validator.js +158 -0
  20. package/frigg-cli/utils/error-messages.js +257 -0
  21. package/frigg-cli/utils/prisma-runner.js +280 -0
  22. package/infrastructure/CLAUDE.md +481 -0
  23. package/infrastructure/IAM-POLICY-TEMPLATES.md +30 -12
  24. package/infrastructure/create-frigg-infrastructure.js +0 -2
  25. package/infrastructure/iam-generator.js +18 -38
  26. package/infrastructure/iam-generator.test.js +40 -8
  27. package/management-ui/src/App.jsx +1 -85
  28. package/management-ui/src/hooks/useFrigg.jsx +1 -215
  29. package/package.json +6 -6
  30. package/test/index.js +2 -4
  31. package/test/mock-integration.js +4 -14
  32. package/frigg-cli/__tests__/jest.config.js +0 -102
  33. package/frigg-cli/__tests__/utils/command-tester.js +0 -170
  34. package/test/auther-definition-tester.js +0 -125
@@ -1,592 +1,346 @@
1
+ /**
2
+ * Test suite for ui command
3
+ *
4
+ * Tests the management UI startup functionality including:
5
+ * - Repository detection and discovery
6
+ * - Development mode (backend + frontend servers)
7
+ * - Production mode (integrated server)
8
+ * - Browser opening
9
+ * - Error handling (port conflicts, missing repos)
10
+ * - Process management
11
+ */
12
+
13
+ // Mock dependencies BEFORE requiring modules
14
+ jest.mock('open', () => jest.fn());
15
+ jest.mock('../../../utils/process-manager');
16
+ jest.mock('../../../utils/repo-detection', () => ({
17
+ getCurrentRepositoryInfo: jest.fn(),
18
+ discoverFriggRepositories: jest.fn(),
19
+ promptRepositorySelection: jest.fn(),
20
+ formatRepositoryInfo: jest.fn()
21
+ }));
22
+ jest.mock('fs', () => ({
23
+ existsSync: jest.fn()
24
+ }));
25
+
26
+ // Require after mocks
27
+ const open = require('open');
28
+ const ProcessManager = require('../../../utils/process-manager');
29
+ const {
30
+ getCurrentRepositoryInfo,
31
+ discoverFriggRepositories,
32
+ formatRepositoryInfo
33
+ } = require('../../../utils/repo-detection');
1
34
  const { uiCommand } = require('../../../ui-command');
2
- const { CommandTester } = require('../../utils/command-tester');
3
- const { MockFactory } = require('../../utils/mock-factory');
4
- const { TestFixtures } = require('../../utils/test-fixtures');
5
35
 
6
36
  describe('CLI Command: ui', () => {
7
- let commandTester;
8
- let mocks;
9
-
10
- beforeEach(() => {
11
- mocks = MockFactory.createMockEnvironment();
12
- commandTester = new CommandTester({
13
- name: 'ui',
14
- description: 'Start the Frigg Management UI',
15
- action: uiCommand,
16
- options: [
17
- { flags: '-p, --port <number>', description: 'port number', defaultValue: '3001' },
18
- { flags: '--no-open', description: 'do not open browser automatically' },
19
- { flags: '-r, --repo <path>', description: 'path to Frigg repository' },
20
- { flags: '--dev', description: 'run in development mode' },
21
- { flags: '--app-path <path>', description: 'path to Frigg application directory' },
22
- { flags: '--config <path>', description: 'path to Frigg configuration file' },
23
- { flags: '--app <path>', description: 'alias for --app-path' }
24
- ]
25
- });
26
- });
37
+ let consoleLogSpy;
38
+ let consoleErrorSpy;
39
+ let processExitSpy;
40
+ let mockProcessManager;
41
+ let originalEnv;
42
+ let stdinResumeSpy;
27
43
 
28
- afterEach(() => {
44
+ beforeEach(() => {
29
45
  jest.clearAllMocks();
30
- commandTester.reset();
31
- });
32
-
33
- describe('Success Cases', () => {
34
- it('should successfully start UI with default settings', async () => {
35
- // Arrange
36
- commandTester
37
- .mock('@friggframework/core', {
38
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
39
- validateBackendPath: jest.fn().mockReturnValue(true)
40
- })
41
- .mock('./ui-command/server', {
42
- startUIServer: jest.fn().mockResolvedValue({
43
- success: true,
44
- port: 3001,
45
- url: 'http://localhost:3001'
46
- })
47
- })
48
- .mock('./ui-command/browser', {
49
- openBrowser: jest.fn().mockResolvedValue(true)
50
- })
51
- .mock('./ui-command/logger', mocks.logger);
52
-
53
- // Act
54
- const result = await commandTester.execute([]);
55
-
56
- // Assert
57
- expect(result.success).toBe(true);
58
- expect(result.exitCode).toBe(0);
59
- });
60
46
 
61
- it('should successfully start UI with custom port', async () => {
62
- // Arrange
63
- const customPort = '8080';
64
-
65
- commandTester
66
- .mock('@friggframework/core', {
67
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
68
- validateBackendPath: jest.fn().mockReturnValue(true)
69
- })
70
- .mock('./ui-command/server', {
71
- startUIServer: jest.fn().mockResolvedValue({
72
- success: true,
73
- port: 8080,
74
- url: 'http://localhost:8080'
75
- })
76
- })
77
- .mock('./ui-command/browser', {
78
- openBrowser: jest.fn().mockResolvedValue(true)
79
- })
80
- .mock('./ui-command/logger', mocks.logger);
47
+ // Mock console methods
48
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
49
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
81
50
 
82
- // Act
83
- const result = await commandTester.execute(['--port', customPort]);
51
+ // Mock process.exit to prevent actual exit
52
+ processExitSpy = jest.spyOn(process, 'exit').mockImplementation();
84
53
 
85
- // Assert
86
- expect(result.success).toBe(true);
87
- expect(result.exitCode).toBe(0);
88
- });
54
+ // Mock process.stdin.resume
55
+ stdinResumeSpy = jest.spyOn(process.stdin, 'resume').mockImplementation();
89
56
 
90
- it('should successfully start UI without opening browser', async () => {
91
- // Arrange
92
- commandTester
93
- .mock('@friggframework/core', {
94
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
95
- validateBackendPath: jest.fn().mockReturnValue(true)
96
- })
97
- .mock('./ui-command/server', {
98
- startUIServer: jest.fn().mockResolvedValue({
99
- success: true,
100
- port: 3001,
101
- url: 'http://localhost:3001'
102
- })
103
- })
104
- .mock('./ui-command/browser', {
105
- openBrowser: jest.fn() // Should not be called
106
- })
107
- .mock('./ui-command/logger', mocks.logger);
57
+ // Store and reset NODE_ENV
58
+ originalEnv = process.env.NODE_ENV;
59
+ delete process.env.NODE_ENV;
108
60
 
109
- // Act
110
- const result = await commandTester.execute(['--no-open']);
61
+ // Setup ProcessManager mock
62
+ mockProcessManager = {
63
+ spawnProcess: jest.fn(),
64
+ printStatus: jest.fn()
65
+ };
66
+ ProcessManager.mockImplementation(() => mockProcessManager);
111
67
 
112
- // Assert
113
- expect(result.success).toBe(true);
114
- expect(result.exitCode).toBe(0);
68
+ // Setup default repo-detection mocks
69
+ getCurrentRepositoryInfo.mockResolvedValue({
70
+ path: '/mock/frigg-repo',
71
+ name: 'test-repo',
72
+ currentSubPath: null
115
73
  });
74
+ formatRepositoryInfo.mockReturnValue('test-repo');
75
+ });
116
76
 
117
- it('should successfully start UI with custom repository path', async () => {
118
- // Arrange
119
- const customRepo = '/custom/repo/path';
120
-
121
- commandTester
122
- .mock('@friggframework/core', {
123
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/custom/backend/package.json'),
124
- validateBackendPath: jest.fn().mockReturnValue(true)
125
- })
126
- .mock('./ui-command/server', {
127
- startUIServer: jest.fn().mockResolvedValue({
128
- success: true,
129
- port: 3001,
130
- url: 'http://localhost:3001',
131
- repo: customRepo
132
- })
133
- })
134
- .mock('./ui-command/browser', {
135
- openBrowser: jest.fn().mockResolvedValue(true)
136
- })
137
- .mock('./ui-command/logger', mocks.logger);
77
+ afterEach(() => {
78
+ consoleLogSpy.mockRestore();
79
+ consoleErrorSpy.mockRestore();
80
+ processExitSpy.mockRestore();
81
+ stdinResumeSpy.mockRestore();
82
+ process.env.NODE_ENV = originalEnv;
83
+ });
138
84
 
139
- // Act
140
- const result = await commandTester.execute(['--repo', customRepo]);
85
+ describe('Repository Detection', () => {
86
+ it('should use specified repo path when provided', async () => {
87
+ await uiCommand({ repo: '/custom/repo', port: 3001, open: false });
141
88
 
142
- // Assert
143
- expect(result.success).toBe(true);
144
- expect(result.exitCode).toBe(0);
89
+ expect(consoleLogSpy).toHaveBeenCalledWith(
90
+ expect.stringContaining('Using specified repository')
91
+ );
145
92
  });
146
93
 
147
- it('should successfully start UI in development mode', async () => {
148
- // Arrange
149
- commandTester
150
- .mock('@friggframework/core', {
151
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
152
- validateBackendPath: jest.fn().mockReturnValue(true)
153
- })
154
- .mock('./ui-command/server', {
155
- startUIServer: jest.fn().mockResolvedValue({
156
- success: true,
157
- port: 3001,
158
- url: 'http://localhost:3001',
159
- development: true
160
- })
161
- })
162
- .mock('./ui-command/browser', {
163
- openBrowser: jest.fn().mockResolvedValue(true)
164
- })
165
- .mock('./ui-command/logger', mocks.logger);
166
-
167
- // Act
168
- const result = await commandTester.execute(['--dev']);
94
+ it('should detect current Frigg repository', async () => {
95
+ await uiCommand({ port: 3001, open: false });
169
96
 
170
- // Assert
171
- expect(result.success).toBe(true);
172
- expect(result.exitCode).toBe(0);
97
+ expect(getCurrentRepositoryInfo).toHaveBeenCalled();
98
+ expect(consoleLogSpy).toHaveBeenCalledWith(
99
+ expect.stringContaining('Found Frigg repository')
100
+ );
173
101
  });
174
102
 
175
- it('should handle WebSocket connections properly', async () => {
176
- // Arrange
177
- commandTester
178
- .mock('@friggframework/core', {
179
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
180
- validateBackendPath: jest.fn().mockReturnValue(true)
181
- })
182
- .mock('./ui-command/server', {
183
- startUIServer: jest.fn().mockResolvedValue({
184
- success: true,
185
- port: 3001,
186
- url: 'http://localhost:3001',
187
- websocket: true
188
- })
189
- })
190
- .mock('./ui-command/browser', {
191
- openBrowser: jest.fn().mockResolvedValue(true)
192
- })
193
- .mock('./ui-command/logger', mocks.logger);
103
+ it('should show subdirectory when in subpath', async () => {
104
+ getCurrentRepositoryInfo.mockResolvedValue({
105
+ path: '/mock/frigg-repo',
106
+ name: 'test-repo',
107
+ currentSubPath: 'packages/backend'
108
+ });
194
109
 
195
- // Act
196
- const result = await commandTester.execute([]);
110
+ await uiCommand({ port: 3001, open: false });
197
111
 
198
- // Assert
199
- expect(result.success).toBe(true);
200
- expect(result.exitCode).toBe(0);
112
+ expect(consoleLogSpy).toHaveBeenCalledWith(
113
+ expect.stringContaining('Currently in subdirectory: packages/backend')
114
+ );
201
115
  });
202
- });
203
116
 
204
- describe('Error Cases', () => {
205
- it('should handle port already in use error', async () => {
206
- // Arrange
207
- commandTester
208
- .mock('@friggframework/core', {
209
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
210
- validateBackendPath: jest.fn().mockReturnValue(true)
211
- })
212
- .mock('./ui-command/server', {
213
- startUIServer: jest.fn().mockRejectedValue(new Error('EADDRINUSE: address already in use :::3001'))
214
- })
215
- .mock('./ui-command/logger', mocks.logger);
117
+ it('should discover repos when not in Frigg repo', async () => {
118
+ getCurrentRepositoryInfo.mockResolvedValue(null);
119
+ discoverFriggRepositories.mockResolvedValue([
120
+ { path: '/repos/frigg-1', name: 'frigg-1' },
121
+ { path: '/repos/frigg-2', name: 'frigg-2' }
122
+ ]);
216
123
 
217
- // Act
218
- const result = await commandTester.execute([]);
124
+ await uiCommand({ port: 3001, open: false });
219
125
 
220
- // Assert
221
- expect(result.success).toBe(false);
222
- expect(result.exitCode).toBe(1);
126
+ expect(discoverFriggRepositories).toHaveBeenCalled();
127
+ expect(consoleLogSpy).toHaveBeenCalledWith(
128
+ expect.stringContaining('Found 2 Frigg repositories')
129
+ );
223
130
  });
224
131
 
225
- it('should handle invalid port number', async () => {
226
- // Arrange
227
- commandTester
228
- .mock('@friggframework/core', {
229
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
230
- validateBackendPath: jest.fn().mockReturnValue(true)
231
- })
232
- .mock('./ui-command/server', {
233
- startUIServer: jest.fn().mockRejectedValue(new Error('Invalid port number'))
234
- })
235
- .mock('./ui-command/logger', mocks.logger);
132
+ it('should exit when no repos found', async () => {
133
+ getCurrentRepositoryInfo.mockResolvedValue(null);
134
+ discoverFriggRepositories.mockResolvedValue([]);
236
135
 
237
- // Act
238
- const result = await commandTester.execute(['--port', '99999']);
136
+ await uiCommand({ port: 3001, open: false });
239
137
 
240
- // Assert
241
- expect(result.success).toBe(false);
242
- expect(result.exitCode).toBe(1);
138
+ expect(consoleLogSpy).toHaveBeenCalledWith(
139
+ expect.stringContaining('No Frigg repositories found')
140
+ );
141
+ expect(processExitSpy).toHaveBeenCalledWith(1);
243
142
  });
143
+ });
244
144
 
245
- it('should handle invalid repository path', async () => {
246
- // Arrange
247
- commandTester
248
- .mock('@friggframework/core', {
249
- findNearestBackendPackageJson: jest.fn().mockReturnValue(null),
250
- validateBackendPath: jest.fn().mockImplementation(() => {
251
- throw new Error('Invalid repository path');
145
+ describe('Development Mode', () => {
146
+ it('should spawn backend and frontend in dev mode', async () => {
147
+ await uiCommand({ port: 3001, open: false, dev: true });
148
+
149
+ expect(mockProcessManager.spawnProcess).toHaveBeenCalledTimes(2);
150
+ expect(mockProcessManager.spawnProcess).toHaveBeenCalledWith(
151
+ 'backend',
152
+ 'npm',
153
+ ['run', 'server'],
154
+ expect.objectContaining({
155
+ cwd: expect.stringContaining('management-ui'),
156
+ env: expect.objectContaining({
157
+ PORT: 3001,
158
+ VITE_API_URL: 'http://localhost:3001'
252
159
  })
253
160
  })
254
- .mock('./ui-command/logger', mocks.logger);
255
-
256
- // Act
257
- const result = await commandTester.execute(['--repo', '/invalid/path']);
258
-
259
- // Assert
260
- expect(result.success).toBe(false);
261
- expect(result.exitCode).toBe(1);
161
+ );
162
+ expect(mockProcessManager.spawnProcess).toHaveBeenCalledWith(
163
+ 'frontend',
164
+ 'npm',
165
+ ['run', 'dev'],
166
+ expect.any(Object)
167
+ );
262
168
  });
263
169
 
264
- it('should handle missing dependencies error', async () => {
265
- // Arrange
266
- commandTester
267
- .mock('@friggframework/core', {
268
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
269
- validateBackendPath: jest.fn().mockReturnValue(true)
270
- })
271
- .mock('./ui-command/server', {
272
- startUIServer: jest.fn().mockRejectedValue(new Error('Missing dependencies: express, socket.io'))
273
- })
274
- .mock('./ui-command/logger', mocks.logger);
275
-
276
- // Act
277
- const result = await commandTester.execute([]);
278
-
279
- // Assert
280
- expect(result.success).toBe(false);
281
- expect(result.exitCode).toBe(1);
282
- });
170
+ it('should use default port 3001', async () => {
171
+ await uiCommand({ open: false, dev: true });
283
172
 
284
- it('should handle server startup timeout', async () => {
285
- // Arrange
286
- commandTester
287
- .mock('@friggframework/core', {
288
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
289
- validateBackendPath: jest.fn().mockReturnValue(true)
290
- })
291
- .mock('./ui-command/server', {
292
- startUIServer: jest.fn().mockRejectedValue(new Error('Server startup timeout'))
173
+ expect(mockProcessManager.spawnProcess).toHaveBeenCalledWith(
174
+ expect.any(String),
175
+ expect.any(String),
176
+ expect.any(Array),
177
+ expect.objectContaining({
178
+ env: expect.objectContaining({ PORT: 3001 })
293
179
  })
294
- .mock('./ui-command/logger', mocks.logger);
295
-
296
- // Act
297
- const result = await commandTester.execute([]);
298
-
299
- // Assert
300
- expect(result.success).toBe(false);
301
- expect(result.exitCode).toBe(1);
180
+ );
302
181
  });
303
182
 
304
- it('should handle browser opening failure gracefully', async () => {
305
- // Arrange
306
- commandTester
307
- .mock('@friggframework/core', {
308
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
309
- validateBackendPath: jest.fn().mockReturnValue(true)
310
- })
311
- .mock('./ui-command/server', {
312
- startUIServer: jest.fn().mockResolvedValue({
313
- success: true,
314
- port: 3001,
315
- url: 'http://localhost:3001'
183
+ it('should use custom port when specified', async () => {
184
+ await uiCommand({ port: 4000, open: false, dev: true });
185
+
186
+ expect(mockProcessManager.spawnProcess).toHaveBeenCalledWith(
187
+ expect.any(String),
188
+ expect.any(String),
189
+ expect.any(Array),
190
+ expect.objectContaining({
191
+ env: expect.objectContaining({
192
+ PORT: 4000,
193
+ VITE_API_URL: 'http://localhost:4000'
316
194
  })
317
195
  })
318
- .mock('./ui-command/browser', {
319
- openBrowser: jest.fn().mockRejectedValue(new Error('Browser not found'))
320
- })
321
- .mock('./ui-command/logger', mocks.logger);
322
-
323
- // Act
324
- const result = await commandTester.execute([]);
325
-
326
- // Assert
327
- expect(result.success).toBe(true); // Should still succeed even if browser fails
328
- expect(result.exitCode).toBe(0);
196
+ );
329
197
  });
330
- });
331
198
 
332
- describe('Edge Cases', () => {
333
- it('should handle system with no available ports', async () => {
334
- // Arrange
335
- commandTester
336
- .mock('@friggframework/core', {
337
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
338
- validateBackendPath: jest.fn().mockReturnValue(true)
339
- })
340
- .mock('./ui-command/server', {
341
- startUIServer: jest.fn().mockRejectedValue(new Error('No available ports'))
342
- })
343
- .mock('./ui-command/logger', mocks.logger);
199
+ it('should print status with correct URLs', async () => {
200
+ await uiCommand({ port: 3001, open: false, dev: true });
344
201
 
345
- // Act
346
- const result = await commandTester.execute([]);
202
+ // Wait for startup delay (testing async behavior, not mocks)
203
+ await new Promise(resolve => setTimeout(resolve, 2100));
347
204
 
348
- // Assert
349
- expect(result.success).toBe(false);
350
- expect(result.exitCode).toBe(1);
205
+ // Verify printStatus called with exact URLs
206
+ expect(mockProcessManager.printStatus).toHaveBeenCalledWith(
207
+ 'http://localhost:5173',
208
+ 'http://localhost:3001',
209
+ 'test-repo'
210
+ );
351
211
  });
352
212
 
353
- it('should handle headless environment (no display)', async () => {
354
- // Arrange
355
- commandTester
356
- .withEnv({ DISPLAY: '' })
357
- .mock('@friggframework/core', {
358
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
359
- validateBackendPath: jest.fn().mockReturnValue(true)
360
- })
361
- .mock('./ui-command/server', {
362
- startUIServer: jest.fn().mockResolvedValue({
363
- success: true,
364
- port: 3001,
365
- url: 'http://localhost:3001'
366
- })
367
- })
368
- .mock('./ui-command/browser', {
369
- openBrowser: jest.fn().mockRejectedValue(new Error('No display available'))
370
- })
371
- .mock('./ui-command/logger', mocks.logger);
213
+ it('should open browser when open=true', async () => {
214
+ await uiCommand({ port: 3001, open: true, dev: true });
372
215
 
373
- // Act
374
- const result = await commandTester.execute([]);
216
+ // Wait for startup + browser delay (testing async behavior, not mocks)
217
+ await new Promise(resolve => setTimeout(resolve, 3100));
375
218
 
376
- // Assert
377
- expect(result.success).toBe(true);
378
- expect(result.exitCode).toBe(0);
219
+ // Verify browser opened with correct URL
220
+ expect(open).toHaveBeenCalledWith('http://localhost:5173');
379
221
  });
380
222
 
381
- it('should handle very large repository', async () => {
382
- // Arrange
383
- commandTester
384
- .mock('@friggframework/core', {
385
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
386
- validateBackendPath: jest.fn().mockReturnValue(true)
387
- })
388
- .mock('./ui-command/server', {
389
- startUIServer: jest.fn().mockResolvedValue({
390
- success: true,
391
- port: 3001,
392
- url: 'http://localhost:3001',
393
- loadTime: 10000, // 10 seconds
394
- size: '1GB'
395
- })
396
- })
397
- .mock('./ui-command/browser', {
398
- openBrowser: jest.fn().mockResolvedValue(true)
399
- })
400
- .mock('./ui-command/logger', mocks.logger);
223
+ it('should NOT open browser when open=false', async () => {
224
+ await uiCommand({ port: 3001, open: false, dev: true });
401
225
 
402
- // Act
403
- const result = await commandTester.execute([]);
226
+ // Wait for startup delay (testing async behavior, not mocks)
227
+ await new Promise(resolve => setTimeout(resolve, 3100));
404
228
 
405
- // Assert
406
- expect(result.success).toBe(true);
407
- expect(result.exitCode).toBe(0);
229
+ // Verify browser was not opened
230
+ expect(open).not.toHaveBeenCalled();
408
231
  });
409
232
 
410
- it('should handle concurrent UI instances', async () => {
411
- // Arrange
412
- commandTester
413
- .mock('@friggframework/core', {
414
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
415
- validateBackendPath: jest.fn().mockReturnValue(true)
416
- })
417
- .mock('./ui-command/server', {
418
- startUIServer: jest.fn().mockResolvedValue({
419
- success: true,
420
- port: 3002, // Different port due to 3001 being used
421
- url: 'http://localhost:3002'
233
+ it('should set PROJECT_ROOT environment variable', async () => {
234
+ getCurrentRepositoryInfo.mockResolvedValue({
235
+ path: '/custom/repo/path',
236
+ name: 'custom-repo',
237
+ currentSubPath: null
238
+ });
239
+
240
+ await uiCommand({ port: 3001, open: false, dev: true });
241
+
242
+ expect(mockProcessManager.spawnProcess).toHaveBeenCalledWith(
243
+ expect.any(String),
244
+ expect.any(String),
245
+ expect.any(Array),
246
+ expect.objectContaining({
247
+ env: expect.objectContaining({
248
+ PROJECT_ROOT: '/custom/repo/path'
422
249
  })
423
250
  })
424
- .mock('./ui-command/browser', {
425
- openBrowser: jest.fn().mockResolvedValue(true)
426
- })
427
- .mock('./ui-command/logger', mocks.logger);
251
+ );
252
+ });
253
+
254
+ it('should set REPOSITORY_INFO as JSON string with correct structure', async () => {
255
+ await uiCommand({ port: 3001, open: false, dev: true });
428
256
 
429
- // Act
430
- const result = await commandTester.execute([]);
257
+ const call = mockProcessManager.spawnProcess.mock.calls[0];
258
+ const env = call[3].env;
431
259
 
432
- // Assert
433
- expect(result.success).toBe(true);
434
- expect(result.exitCode).toBe(0);
260
+ // Verify REPOSITORY_INFO is valid JSON with correct structure
261
+ expect(env.REPOSITORY_INFO).toBeDefined();
262
+ const parsedInfo = JSON.parse(env.REPOSITORY_INFO);
263
+
264
+ // Verify actual data matches mock
265
+ expect(parsedInfo).toEqual({
266
+ path: '/mock/frigg-repo',
267
+ name: 'test-repo',
268
+ currentSubPath: null
269
+ });
435
270
  });
436
- });
437
271
 
438
- describe('Option Validation', () => {
439
- it('should validate port number range', async () => {
440
- // Arrange
441
- const validPorts = ['3000', '8080', '9000'];
442
-
443
- for (const port of validPorts) {
444
- commandTester
445
- .mock('@friggframework/core', {
446
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
447
- validateBackendPath: jest.fn().mockReturnValue(true)
448
- })
449
- .mock('./ui-command/server', {
450
- startUIServer: jest.fn().mockResolvedValue({
451
- success: true,
452
- port: parseInt(port),
453
- url: `http://localhost:${port}`
454
- })
455
- })
456
- .mock('./ui-command/browser', {
457
- openBrowser: jest.fn().mockResolvedValue(true)
458
- })
459
- .mock('./ui-command/logger', mocks.logger);
272
+ it('should set AVAILABLE_REPOSITORIES with actual repository data', async () => {
273
+ getCurrentRepositoryInfo.mockResolvedValue(null);
274
+ discoverFriggRepositories.mockResolvedValue([
275
+ { path: '/repos/frigg-1', name: 'frigg-1' }
276
+ ]);
460
277
 
461
- // Act
462
- const result = await commandTester.execute(['--port', port]);
278
+ await uiCommand({ port: 3001, open: false, dev: true });
463
279
 
464
- // Assert
465
- expect(result.success).toBe(true);
466
- expect(result.exitCode).toBe(0);
467
- }
468
- });
280
+ const call = mockProcessManager.spawnProcess.mock.calls[0];
281
+ const env = call[3].env;
469
282
 
470
- it('should handle short port option (-p)', async () => {
471
- // Arrange
472
- commandTester
473
- .mock('@friggframework/core', {
474
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
475
- validateBackendPath: jest.fn().mockReturnValue(true)
476
- })
477
- .mock('./ui-command/server', {
478
- startUIServer: jest.fn().mockResolvedValue({
479
- success: true,
480
- port: 9000,
481
- url: 'http://localhost:9000'
482
- })
483
- })
484
- .mock('./ui-command/browser', {
485
- openBrowser: jest.fn().mockResolvedValue(true)
486
- })
487
- .mock('./ui-command/logger', mocks.logger);
283
+ // Verify AVAILABLE_REPOSITORIES is valid JSON with correct structure
284
+ expect(env.AVAILABLE_REPOSITORIES).toBeDefined();
285
+ const parsed = JSON.parse(env.AVAILABLE_REPOSITORIES);
488
286
 
489
- // Act
490
- const result = await commandTester.execute(['-p', '9000']);
287
+ // Verify actual data matches mock
288
+ expect(parsed).toEqual([
289
+ { path: '/repos/frigg-1', name: 'frigg-1' }
290
+ ]);
291
+ });
292
+ });
491
293
 
492
- // Assert
493
- expect(result.success).toBe(true);
494
- expect(result.exitCode).toBe(0);
294
+ describe('Error Handling', () => {
295
+ it('should handle EADDRINUSE error', async () => {
296
+ mockProcessManager.spawnProcess.mockImplementation(() => {
297
+ const error = new Error('Port in use');
298
+ error.code = 'EADDRINUSE';
299
+ throw error;
300
+ });
301
+
302
+ await uiCommand({ port: 3001, open: false, dev: true });
303
+
304
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
305
+ expect.stringContaining('Failed to start Management UI'),
306
+ expect.any(String)
307
+ );
308
+ expect(consoleLogSpy).toHaveBeenCalledWith(
309
+ expect.stringContaining('Port 3001 is already in use')
310
+ );
311
+ expect(processExitSpy).toHaveBeenCalledWith(1);
495
312
  });
496
313
 
497
- it('should handle short repo option (-r)', async () => {
498
- // Arrange
499
- const customRepo = '/custom/repo';
500
-
501
- commandTester
502
- .mock('@friggframework/core', {
503
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/custom/backend/package.json'),
504
- validateBackendPath: jest.fn().mockReturnValue(true)
505
- })
506
- .mock('./ui-command/server', {
507
- startUIServer: jest.fn().mockResolvedValue({
508
- success: true,
509
- port: 3001,
510
- url: 'http://localhost:3001',
511
- repo: customRepo
512
- })
513
- })
514
- .mock('./ui-command/browser', {
515
- openBrowser: jest.fn().mockResolvedValue(true)
516
- })
517
- .mock('./ui-command/logger', mocks.logger);
314
+ it('should handle generic errors', async () => {
315
+ mockProcessManager.spawnProcess.mockImplementation(() => {
316
+ throw new Error('Generic startup error');
317
+ });
518
318
 
519
- // Act
520
- const result = await commandTester.execute(['-r', customRepo]);
319
+ await uiCommand({ port: 3001, open: false, dev: true });
521
320
 
522
- // Assert
523
- expect(result.success).toBe(true);
524
- expect(result.exitCode).toBe(0);
321
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
322
+ expect.stringContaining('Failed to start Management UI'),
323
+ 'Generic startup error'
324
+ );
325
+ expect(processExitSpy).toHaveBeenCalledWith(1);
525
326
  });
327
+ });
526
328
 
527
- it('should handle combined options', async () => {
528
- // Arrange
529
- commandTester
530
- .mock('@friggframework/core', {
531
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/custom/backend/package.json'),
532
- validateBackendPath: jest.fn().mockReturnValue(true)
533
- })
534
- .mock('./ui-command/server', {
535
- startUIServer: jest.fn().mockResolvedValue({
536
- success: true,
537
- port: 8080,
538
- url: 'http://localhost:8080',
539
- development: true
540
- })
541
- })
542
- .mock('./ui-command/browser', {
543
- openBrowser: jest.fn() // Should not be called due to --no-open
544
- })
545
- .mock('./ui-command/logger', mocks.logger);
546
-
547
- // Act
548
- const result = await commandTester.execute([
549
- '--port', '8080',
550
- '--dev',
551
- '--no-open',
552
- '--repo', '/custom/repo'
553
- ]);
329
+ describe('Process Management', () => {
330
+ it('should create ProcessManager instance', async () => {
331
+ await uiCommand({ port: 3001, open: false, dev: true });
554
332
 
555
- // Assert
556
- expect(result.success).toBe(true);
557
- expect(result.exitCode).toBe(0);
333
+ expect(ProcessManager).toHaveBeenCalled();
558
334
  });
559
- });
560
335
 
561
- describe('Performance Tests', () => {
562
- it('should start UI server within reasonable time', async () => {
563
- // Arrange
564
- const startTime = Date.now();
565
-
566
- commandTester
567
- .mock('@friggframework/core', {
568
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
569
- validateBackendPath: jest.fn().mockReturnValue(true)
570
- })
571
- .mock('./ui-command/server', {
572
- startUIServer: jest.fn().mockResolvedValue({
573
- success: true,
574
- port: 3001,
575
- url: 'http://localhost:3001'
576
- })
577
- })
578
- .mock('./ui-command/browser', {
579
- openBrowser: jest.fn().mockResolvedValue(true)
580
- })
581
- .mock('./ui-command/logger', mocks.logger);
336
+ it('should keep process running with stdin.resume', async () => {
337
+ await uiCommand({ port: 3001, open: false, dev: true });
582
338
 
583
- // Act
584
- const result = await commandTester.execute([]);
585
- const endTime = Date.now();
339
+ // Wait for startup delay (testing async behavior, not mocks)
340
+ await new Promise(resolve => setTimeout(resolve, 2100));
586
341
 
587
- // Assert
588
- expect(result.success).toBe(true);
589
- expect(endTime - startTime).toBeLessThan(3000); // Should start within 3 seconds
342
+ // Verify stdin.resume called to keep process alive
343
+ expect(stdinResumeSpy).toHaveBeenCalled();
590
344
  });
591
345
  });
592
- });
346
+ });