@friggframework/devtools 2.0.0-next.40 → 2.0.0-next.42
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/frigg-cli/__tests__/unit/commands/build.test.js +173 -405
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +359 -377
- package/frigg-cli/__tests__/unit/commands/ui.test.js +266 -512
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +486 -0
- package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
- package/frigg-cli/__tests__/utils/test-setup.js +22 -21
- package/frigg-cli/db-setup-command/index.js +186 -0
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +151 -162
- package/frigg-cli/generate-iam-command.js +7 -4
- package/frigg-cli/index.js +9 -1
- package/frigg-cli/install-command/index.js +1 -1
- package/frigg-cli/jest.config.js +124 -0
- package/frigg-cli/package.json +4 -1
- package/frigg-cli/start-command/index.js +101 -2
- package/frigg-cli/start-command/start-command.test.js +297 -0
- package/frigg-cli/utils/database-validator.js +158 -0
- package/frigg-cli/utils/error-messages.js +257 -0
- package/frigg-cli/utils/prisma-runner.js +280 -0
- package/infrastructure/CLAUDE.md +481 -0
- package/infrastructure/IAM-POLICY-TEMPLATES.md +30 -12
- package/infrastructure/create-frigg-infrastructure.js +0 -2
- package/infrastructure/iam-generator.js +18 -38
- package/infrastructure/iam-generator.test.js +40 -8
- package/infrastructure/serverless-template.js +25 -4
- package/infrastructure/serverless-template.test.js +45 -0
- package/package.json +6 -6
- package/test/index.js +2 -4
- package/test/mock-integration.js +4 -14
- package/frigg-cli/__tests__/jest.config.js +0 -102
- package/frigg-cli/__tests__/utils/command-tester.js +0 -170
- 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
|
|
8
|
-
let
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
83
|
-
|
|
51
|
+
// Mock process.exit to prevent actual exit
|
|
52
|
+
processExitSpy = jest.spyOn(process, 'exit').mockImplementation();
|
|
84
53
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
expect(result.exitCode).toBe(0);
|
|
88
|
-
});
|
|
54
|
+
// Mock process.stdin.resume
|
|
55
|
+
stdinResumeSpy = jest.spyOn(process.stdin, 'resume').mockImplementation();
|
|
89
56
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
110
|
-
|
|
61
|
+
// Setup ProcessManager mock
|
|
62
|
+
mockProcessManager = {
|
|
63
|
+
spawnProcess: jest.fn(),
|
|
64
|
+
printStatus: jest.fn()
|
|
65
|
+
};
|
|
66
|
+
ProcessManager.mockImplementation(() => mockProcessManager);
|
|
111
67
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
89
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
90
|
+
expect.stringContaining('Using specified repository')
|
|
91
|
+
);
|
|
145
92
|
});
|
|
146
93
|
|
|
147
|
-
it('should
|
|
148
|
-
|
|
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
|
-
|
|
171
|
-
expect(
|
|
172
|
-
|
|
97
|
+
expect(getCurrentRepositoryInfo).toHaveBeenCalled();
|
|
98
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
99
|
+
expect.stringContaining('Found Frigg repository')
|
|
100
|
+
);
|
|
173
101
|
});
|
|
174
102
|
|
|
175
|
-
it('should
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
196
|
-
const result = await commandTester.execute([]);
|
|
110
|
+
await uiCommand({ port: 3001, open: false });
|
|
197
111
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
112
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
113
|
+
expect.stringContaining('Currently in subdirectory: packages/backend')
|
|
114
|
+
);
|
|
201
115
|
});
|
|
202
|
-
});
|
|
203
116
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
218
|
-
const result = await commandTester.execute([]);
|
|
124
|
+
await uiCommand({ port: 3001, open: false });
|
|
219
125
|
|
|
220
|
-
|
|
221
|
-
expect(
|
|
222
|
-
|
|
126
|
+
expect(discoverFriggRepositories).toHaveBeenCalled();
|
|
127
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
128
|
+
expect.stringContaining('Found 2 Frigg repositories')
|
|
129
|
+
);
|
|
223
130
|
});
|
|
224
131
|
|
|
225
|
-
it('should
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
238
|
-
const result = await commandTester.execute(['--port', '99999']);
|
|
136
|
+
await uiCommand({ port: 3001, open: false });
|
|
239
137
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
138
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
139
|
+
expect.stringContaining('No Frigg repositories found')
|
|
140
|
+
);
|
|
141
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
243
142
|
});
|
|
143
|
+
});
|
|
244
144
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
265
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
.
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
//
|
|
346
|
-
|
|
202
|
+
// Wait for startup delay (testing async behavior, not mocks)
|
|
203
|
+
await new Promise(resolve => setTimeout(resolve, 2100));
|
|
347
204
|
|
|
348
|
-
//
|
|
349
|
-
expect(
|
|
350
|
-
|
|
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
|
|
354
|
-
|
|
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
|
-
//
|
|
374
|
-
|
|
216
|
+
// Wait for startup + browser delay (testing async behavior, not mocks)
|
|
217
|
+
await new Promise(resolve => setTimeout(resolve, 3100));
|
|
375
218
|
|
|
376
|
-
//
|
|
377
|
-
expect(
|
|
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
|
|
382
|
-
|
|
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
|
-
//
|
|
403
|
-
|
|
226
|
+
// Wait for startup delay (testing async behavior, not mocks)
|
|
227
|
+
await new Promise(resolve => setTimeout(resolve, 3100));
|
|
404
228
|
|
|
405
|
-
//
|
|
406
|
-
expect(
|
|
407
|
-
expect(result.exitCode).toBe(0);
|
|
229
|
+
// Verify browser was not opened
|
|
230
|
+
expect(open).not.toHaveBeenCalled();
|
|
408
231
|
});
|
|
409
232
|
|
|
410
|
-
it('should
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
430
|
-
const
|
|
257
|
+
const call = mockProcessManager.spawnProcess.mock.calls[0];
|
|
258
|
+
const env = call[3].env;
|
|
431
259
|
|
|
432
|
-
//
|
|
433
|
-
expect(
|
|
434
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
462
|
-
const result = await commandTester.execute(['--port', port]);
|
|
278
|
+
await uiCommand({ port: 3001, open: false, dev: true });
|
|
463
279
|
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
//
|
|
490
|
-
|
|
287
|
+
// Verify actual data matches mock
|
|
288
|
+
expect(parsed).toEqual([
|
|
289
|
+
{ path: '/repos/frigg-1', name: 'frigg-1' }
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
491
293
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
520
|
-
const result = await commandTester.execute(['-r', customRepo]);
|
|
319
|
+
await uiCommand({ port: 3001, open: false, dev: true });
|
|
521
320
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
556
|
-
expect(result.success).toBe(true);
|
|
557
|
-
expect(result.exitCode).toBe(0);
|
|
333
|
+
expect(ProcessManager).toHaveBeenCalled();
|
|
558
334
|
});
|
|
559
|
-
});
|
|
560
335
|
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
//
|
|
584
|
-
|
|
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
|
-
//
|
|
588
|
-
expect(
|
|
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
|
+
});
|