@friggframework/devtools 2.0.0-next.41 → 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.
Files changed (32) 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/package.json +6 -6
  28. package/test/index.js +2 -4
  29. package/test/mock-integration.js +4 -14
  30. package/frigg-cli/__tests__/jest.config.js +0 -102
  31. package/frigg-cli/__tests__/utils/command-tester.js +0 -170
  32. package/test/auther-definition-tester.js +0 -125
@@ -1,418 +1,400 @@
1
+ /**
2
+ * Test suite for install command
3
+ *
4
+ * Tests the ACTUAL Frigg implementation including:
5
+ * - Package search and selection (mocked - external npm)
6
+ * - Package installation via npm (mocked - external)
7
+ * - Integration file creation (REAL - tests actual file generation)
8
+ * - Backend.js updates (REAL - tests actual file parsing/updating)
9
+ * - Git commits (mocked - external)
10
+ * - Environment variable handling (mocked - interactive)
11
+ * - Label sanitization (REAL - tests actual regex logic)
12
+ */
13
+
14
+ // Mock ONLY external boundaries - let Frigg logic run!
15
+ jest.mock('fs-extra'); // Mock at I/O level
16
+ jest.mock('../../../install-command/install-package', () => ({
17
+ installPackage: jest.fn() // External: npm install
18
+ }));
19
+ jest.mock('../../../install-command/commit-changes', () => ({
20
+ commitChanges: jest.fn() // External: git commands
21
+ }));
22
+ jest.mock('../../../install-command/environment-variables', () => ({
23
+ handleEnvVariables: jest.fn() // External: interactive prompts
24
+ }));
25
+ jest.mock('../../../install-command/validate-package', () => ({
26
+ validatePackageExists: jest.fn(), // External: npm registry
27
+ searchAndSelectPackage: jest.fn() // External: interactive selection
28
+ }));
29
+ jest.mock('@friggframework/core', () => ({
30
+ findNearestBackendPackageJson: jest.fn(),
31
+ validateBackendPath: jest.fn()
32
+ }));
33
+
34
+ // DON'T mock these - let them run to test actual Frigg logic:
35
+ // - createIntegrationFile (tests file generation)
36
+ // - updateBackendJsFile (tests file parsing)
37
+ // - logger (just console.log, we'll spy on console)
38
+ // - getIntegrationTemplate (tests template generation)
39
+
40
+ // Require after mocks
41
+ const fs = require('fs-extra');
42
+ const { installPackage } = require('../../../install-command/install-package');
43
+ const { commitChanges } = require('../../../install-command/commit-changes');
44
+ const { handleEnvVariables } = require('../../../install-command/environment-variables');
45
+ const { validatePackageExists, searchAndSelectPackage } = require('../../../install-command/validate-package');
46
+ const { findNearestBackendPackageJson, validateBackendPath } = require('@friggframework/core');
1
47
  const { installCommand } = require('../../../install-command');
2
- const { CommandTester } = require('../../utils/command-tester');
3
- const { MockFactory } = require('../../utils/mock-factory');
4
- const { TestFixtures } = require('../../utils/test-fixtures');
5
48
 
6
49
  describe('CLI Command: install', () => {
7
- let commandTester;
8
- let mocks;
9
-
50
+ let processExitSpy;
51
+ let consoleLogSpy;
52
+ let consoleErrorSpy;
53
+ const mockBackendPath = '/mock/backend/package.json';
54
+ const mockBackendDir = '/mock/backend';
55
+
10
56
  beforeEach(() => {
11
- mocks = MockFactory.createMockEnvironment();
12
- commandTester = new CommandTester({
13
- name: 'install <apiModuleName>',
14
- description: 'Install an API module',
15
- action: installCommand,
16
- options: [
17
- { flags: '--app-path <path>', description: 'path to Frigg application directory' },
18
- { flags: '--config <path>', description: 'path to Frigg configuration file' },
19
- { flags: '--app <path>', description: 'alias for --app-path' }
20
- ]
21
- });
57
+ jest.clearAllMocks();
58
+
59
+ // Mock process.exit to prevent actual exit
60
+ processExitSpy = jest.spyOn(process, 'exit').mockImplementation();
61
+
62
+ // Spy on console for logger (don't mock logger - test it!)
63
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
64
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
65
+
66
+ // Setup fs-extra mocks - Let Frigg code run, just mock I/O
67
+ fs.ensureDirSync = jest.fn();
68
+ fs.writeFileSync = jest.fn();
69
+ fs.readFileSync = jest.fn().mockReturnValue(`
70
+ // Sample backend.js file
71
+ const integrations = [
72
+ // Existing integrations
73
+ ];
74
+
75
+ module.exports = {
76
+ integrations: []
77
+ };
78
+ `);
79
+ fs.existsSync = jest.fn().mockReturnValue(true);
80
+
81
+ // Setup default successful mocks for external boundaries
82
+ searchAndSelectPackage.mockResolvedValue(['@friggframework/api-module-slack']);
83
+ findNearestBackendPackageJson.mockReturnValue(mockBackendPath);
84
+ validateBackendPath.mockReturnValue(true);
85
+ validatePackageExists.mockResolvedValue(true);
86
+ installPackage.mockReturnValue(undefined);
87
+ handleEnvVariables.mockResolvedValue(undefined);
88
+
89
+ // Mock the dynamic require() of installed package using jest.doMock
90
+ const path = require('path');
91
+ const slackModulePath = path.resolve(mockBackendPath, '../../node_modules/@friggframework/api-module-slack');
92
+
93
+ jest.doMock(slackModulePath, () => ({
94
+ Config: { label: 'Slack' },
95
+ Api: class SlackApi {}
96
+ }), { virtual: true });
22
97
  });
23
98
 
24
99
  afterEach(() => {
25
- jest.clearAllMocks();
26
- commandTester.reset();
100
+ processExitSpy.mockRestore();
101
+ consoleLogSpy.mockRestore();
102
+ consoleErrorSpy.mockRestore();
103
+ jest.resetModules(); // Clear module cache after each test
27
104
  });
28
105
 
29
106
  describe('Success Cases', () => {
30
- it('should successfully install an API module with default configuration', async () => {
31
- // Arrange
32
- const moduleName = 'salesforce';
33
- const expectedPackage = '@friggframework/api-module-salesforce';
34
-
35
- commandTester
36
- .mock('./install-command/validate-package', {
37
- validatePackageExists: jest.fn().mockResolvedValue(true)
38
- })
39
- .mock('@friggframework/core', {
40
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
41
- validateBackendPath: jest.fn().mockReturnValue(true)
42
- })
43
- .mock('./install-command/install-package', {
44
- installPackage: jest.fn().mockResolvedValue(true)
45
- })
46
- .mock('./install-command/integration-file', {
47
- createIntegrationFile: jest.fn().mockResolvedValue(true)
48
- })
49
- .mock('./install-command/backend-js', {
50
- updateBackendJsFile: jest.fn().mockResolvedValue(true)
51
- })
52
- .mock('./install-command/commit-changes', {
53
- commitChanges: jest.fn().mockResolvedValue(true)
54
- })
55
- .mock('./install-command/logger', mocks.logger);
56
-
57
- // Act
58
- const result = await commandTester.execute([moduleName]);
59
-
60
- // Assert
61
- expect(result.success).toBe(true);
62
- expect(result.exitCode).toBe(0);
107
+ it('should orchestrate complete installation workflow', async () => {
108
+ await installCommand('slack');
109
+
110
+ // Verify external boundaries called
111
+ expect(searchAndSelectPackage).toHaveBeenCalledWith('slack');
112
+ expect(findNearestBackendPackageJson).toHaveBeenCalled();
113
+ expect(validateBackendPath).toHaveBeenCalledWith(mockBackendPath);
114
+ expect(validatePackageExists).toHaveBeenCalledWith('@friggframework/api-module-slack');
115
+ expect(installPackage).toHaveBeenCalledWith(mockBackendPath, '@friggframework/api-module-slack');
63
116
  });
64
117
 
65
- it('should successfully install with custom app path', async () => {
66
- // Arrange
67
- const moduleName = 'hubspot';
68
- const customPath = '/custom/app/path';
69
-
70
- commandTester
71
- .mock('./install-command/validate-package', {
72
- validatePackageExists: jest.fn().mockResolvedValue(true)
73
- })
74
- .mock('@friggframework/core', {
75
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/custom/backend/package.json'),
76
- validateBackendPath: jest.fn().mockReturnValue(true)
77
- })
78
- .mock('./install-command/install-package', {
79
- installPackage: jest.fn().mockResolvedValue(true)
80
- })
81
- .mock('./install-command/integration-file', {
82
- createIntegrationFile: jest.fn().mockResolvedValue(true)
83
- })
84
- .mock('./install-command/backend-js', {
85
- updateBackendJsFile: jest.fn().mockResolvedValue(true)
86
- })
87
- .mock('./install-command/commit-changes', {
88
- commitChanges: jest.fn().mockResolvedValue(true)
89
- })
90
- .mock('./install-command/logger', mocks.logger);
91
-
92
- // Act
93
- const result = await commandTester.execute([moduleName, '--app-path', customPath]);
94
-
95
- // Assert
96
- expect(result.success).toBe(true);
97
- expect(result.exitCode).toBe(0);
118
+ it('should create integration file with correct path and content', async () => {
119
+ await installCommand('slack');
120
+
121
+ // Verify directory creation
122
+ expect(fs.ensureDirSync).toHaveBeenCalledWith(
123
+ expect.stringMatching(/src\/integrations$/)
124
+ );
125
+
126
+ // Verify integration file written with correct path
127
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
128
+ expect.stringMatching(/SlackIntegration\.js$/),
129
+ expect.any(String)
130
+ );
131
+
132
+ // Get the actual content that was written
133
+ const writeCall = fs.writeFileSync.mock.calls.find(call =>
134
+ call[0].includes('SlackIntegration.js')
135
+ );
136
+
137
+ expect(writeCall).toBeDefined();
138
+ const [filePath, content] = writeCall;
139
+
140
+ // Verify file content contains valid integration class
141
+ expect(content).toContain('class SlackIntegration extends IntegrationBase');
142
+ expect(content).toContain('@friggframework/core');
143
+ expect(content).toContain('@friggframework/api-module-slack');
98
144
  });
99
145
 
100
- it('should handle module names with special characters', async () => {
101
- // Arrange
102
- const moduleName = 'google-calendar';
103
- const expectedPackage = '@friggframework/api-module-google-calendar';
104
-
105
- commandTester
106
- .mock('./install-command/validate-package', {
107
- validatePackageExists: jest.fn().mockResolvedValue(true)
108
- })
109
- .mock('@friggframework/core', {
110
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
111
- validateBackendPath: jest.fn().mockReturnValue(true)
112
- })
113
- .mock('./install-command/install-package', {
114
- installPackage: jest.fn().mockResolvedValue(true)
115
- })
116
- .mock('./install-command/integration-file', {
117
- createIntegrationFile: jest.fn().mockResolvedValue(true)
118
- })
119
- .mock('./install-command/backend-js', {
120
- updateBackendJsFile: jest.fn().mockResolvedValue(true)
121
- })
122
- .mock('./install-command/commit-changes', {
123
- commitChanges: jest.fn().mockResolvedValue(true)
124
- })
125
- .mock('./install-command/logger', mocks.logger);
126
-
127
- // Act
128
- const result = await commandTester.execute([moduleName]);
129
-
130
- // Assert
131
- expect(result.success).toBe(true);
132
- expect(result.exitCode).toBe(0);
146
+ it('should generate valid JavaScript template', async () => {
147
+ await installCommand('slack');
148
+
149
+ const writeCall = fs.writeFileSync.mock.calls.find(call =>
150
+ call[0].includes('SlackIntegration.js')
151
+ );
152
+
153
+ const [, content] = writeCall;
154
+
155
+ // Verify template has required structure
156
+ expect(content).toMatch(/class \w+Integration extends IntegrationBase/);
157
+ expect(content).toContain('static Config =');
158
+ expect(content).toContain('static Options =');
159
+ expect(content).toContain('static modules =');
160
+
161
+ // Verify template is syntactically valid (no unclosed braces, etc)
162
+ expect(content.split('{').length).toBe(content.split('}').length);
133
163
  });
134
- });
135
164
 
136
- describe('Error Cases', () => {
137
- it('should handle package not found error', async () => {
138
- // Arrange
139
- const moduleName = 'non-existent-module';
140
-
141
- commandTester
142
- .mock('./install-command/validate-package', {
143
- validatePackageExists: jest.fn().mockRejectedValue(new Error('Package not found'))
144
- })
145
- .mock('./install-command/logger', mocks.logger);
146
-
147
- // Act
148
- const result = await commandTester.execute([moduleName]);
149
-
150
- // Assert
151
- expect(result.success).toBe(false);
152
- expect(result.exitCode).toBe(1);
165
+ it('should update backend.js with integration import', async () => {
166
+ await installCommand('slack');
167
+
168
+ // Verify backend.js was read
169
+ expect(fs.readFileSync).toHaveBeenCalledWith(
170
+ expect.stringMatching(/backend\.js$/),
171
+ 'utf-8'
172
+ );
173
+
174
+ // Verify backend.js was written back with import
175
+ const backendWriteCall = fs.writeFileSync.mock.calls.find(call =>
176
+ call[0].includes('backend.js')
177
+ );
178
+
179
+ expect(backendWriteCall).toBeDefined();
180
+ const [, updatedBackend] = backendWriteCall;
181
+
182
+ // Verify import statement added
183
+ expect(updatedBackend).toContain('const SlackIntegration = require');
184
+ expect(updatedBackend).toContain('./src/integrations/SlackIntegration');
185
+
186
+ // Verify integration added to array
187
+ expect(updatedBackend).toContain('SlackIntegration,');
153
188
  });
154
189
 
155
- it('should handle invalid backend path error', async () => {
156
- // Arrange
157
- const moduleName = 'salesforce';
158
-
159
- commandTester
160
- .mock('./install-command/validate-package', {
161
- validatePackageExists: jest.fn().mockResolvedValue(true)
162
- })
163
- .mock('@friggframework/core', {
164
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/invalid/path'),
165
- validateBackendPath: jest.fn().mockImplementation(() => {
166
- throw new Error('Invalid backend path');
167
- })
168
- })
169
- .mock('./install-command/logger', mocks.logger);
170
-
171
- // Act
172
- const result = await commandTester.execute([moduleName]);
173
-
174
- // Assert
175
- expect(result.success).toBe(false);
176
- expect(result.exitCode).toBe(1);
190
+ it('should commit changes after file operations', async () => {
191
+ await installCommand('slack');
192
+
193
+ expect(commitChanges).toHaveBeenCalledWith(mockBackendPath, 'Slack');
177
194
  });
178
195
 
179
- it('should handle installation failure', async () => {
180
- // Arrange
181
- const moduleName = 'salesforce';
182
-
183
- commandTester
184
- .mock('./install-command/validate-package', {
185
- validatePackageExists: jest.fn().mockResolvedValue(true)
186
- })
187
- .mock('@friggframework/core', {
188
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
189
- validateBackendPath: jest.fn().mockReturnValue(true)
190
- })
191
- .mock('./install-command/install-package', {
192
- installPackage: jest.fn().mockRejectedValue(new Error('Installation failed'))
193
- })
194
- .mock('./install-command/logger', mocks.logger);
195
-
196
- // Act
197
- const result = await commandTester.execute([moduleName]);
198
-
199
- // Assert
200
- expect(result.success).toBe(false);
201
- expect(result.exitCode).toBe(1);
196
+ it('should handle environment variables after installation', async () => {
197
+ await installCommand('slack');
198
+
199
+ expect(handleEnvVariables).toHaveBeenCalledWith(
200
+ mockBackendPath,
201
+ expect.stringContaining('@friggframework/api-module-slack')
202
+ );
202
203
  });
203
204
 
204
- it('should handle integration file creation failure', async () => {
205
- // Arrange
206
- const moduleName = 'salesforce';
207
-
208
- commandTester
209
- .mock('./install-command/validate-package', {
210
- validatePackageExists: jest.fn().mockResolvedValue(true)
211
- })
212
- .mock('@friggframework/core', {
213
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
214
- validateBackendPath: jest.fn().mockReturnValue(true)
215
- })
216
- .mock('./install-command/install-package', {
217
- installPackage: jest.fn().mockResolvedValue(true)
218
- })
219
- .mock('./install-command/integration-file', {
220
- createIntegrationFile: jest.fn().mockRejectedValue(new Error('File creation failed'))
221
- })
222
- .mock('./install-command/logger', mocks.logger);
223
-
224
- // Act
225
- const result = await commandTester.execute([moduleName]);
226
-
227
- // Assert
228
- expect(result.success).toBe(false);
229
- expect(result.exitCode).toBe(1);
205
+ it('should log info messages during installation', async () => {
206
+ await installCommand('slack');
207
+
208
+ // Verify logger actually logged (we spy on console)
209
+ expect(consoleLogSpy).toHaveBeenCalledWith(
210
+ expect.stringContaining('Successfully installed @friggframework/api-module-slack')
211
+ );
230
212
  });
231
- });
232
213
 
233
- describe('Edge Cases', () => {
234
- it('should handle empty module name', async () => {
235
- // Arrange
236
- commandTester
237
- .mock('./install-command/logger', mocks.logger);
214
+ it('should install multiple packages sequentially', async () => {
215
+ searchAndSelectPackage.mockResolvedValue([
216
+ '@friggframework/api-module-slack',
217
+ '@friggframework/api-module-hubspot'
218
+ ]);
219
+
220
+ // Mock HubSpot module (Slack already mocked in beforeEach)
221
+ const path = require('path');
222
+ const hubspotPath = path.resolve(mockBackendPath, '../../node_modules/@friggframework/api-module-hubspot');
223
+
224
+ jest.doMock(hubspotPath, () => ({
225
+ Config: { label: 'HubSpot' },
226
+ Api: class HubSpotApi {}
227
+ }), { virtual: true });
228
+
229
+ await installCommand('crm');
238
230
 
239
- // Act
240
- const result = await commandTester.execute(['']);
231
+ expect(validatePackageExists).toHaveBeenCalledTimes(2);
232
+ expect(installPackage).toHaveBeenCalledTimes(2);
241
233
 
242
- // Assert
243
- expect(result.success).toBe(false);
244
- expect(result.exitCode).toBe(1);
234
+ // Verify TWO integration files created
235
+ const integrationFiles = fs.writeFileSync.mock.calls.filter(call =>
236
+ call[0].includes('Integration.js') && !call[0].includes('backend.js')
237
+ );
238
+ expect(integrationFiles.length).toBe(2);
239
+
240
+ // Verify both files have correct names
241
+ expect(integrationFiles[0][0]).toContain('SlackIntegration.js');
242
+ expect(integrationFiles[1][0]).toContain('HubSpotIntegration.js');
245
243
  });
246
244
 
247
- it('should handle module name with invalid characters', async () => {
248
- // Arrange
249
- const moduleName = 'invalid@module#name';
250
-
251
- commandTester
252
- .mock('./install-command/validate-package', {
253
- validatePackageExists: jest.fn().mockRejectedValue(new Error('Invalid package name'))
254
- })
255
- .mock('./install-command/logger', mocks.logger);
256
-
257
- // Act
258
- const result = await commandTester.execute([moduleName]);
259
-
260
- // Assert
261
- expect(result.success).toBe(false);
262
- expect(result.exitCode).toBe(1);
245
+ it('should sanitize label by removing invalid characters', async () => {
246
+ // Mock different package with special characters in label
247
+ searchAndSelectPackage.mockResolvedValue(['@friggframework/api-module-google-drive']);
248
+
249
+ const path = require('path');
250
+ const googleDrivePath = path.resolve(mockBackendPath, '../../node_modules/@friggframework/api-module-google-drive');
251
+
252
+ jest.doMock(googleDrivePath, () => ({
253
+ Config: { label: 'Google<Drive>' }, // Has invalid characters
254
+ Api: class GoogleDriveApi {}
255
+ }), { virtual: true });
256
+
257
+ await installCommand('google-drive');
258
+
259
+ // Verify sanitized label used in file name
260
+ const writeCall = fs.writeFileSync.mock.calls.find(call =>
261
+ call[0].includes('Integration.js')
262
+ );
263
+
264
+ // Should be GoogleDrive, not Google<Drive>
265
+ expect(writeCall[0]).toContain('GoogleDriveIntegration.js');
266
+ expect(writeCall[0]).not.toContain('<');
267
+ expect(writeCall[0]).not.toContain('>');
268
+
269
+ // Verify content uses sanitized name
270
+ expect(writeCall[1]).toContain('class GoogleDriveIntegration');
271
+ expect(writeCall[1]).not.toContain('Google<Drive>');
263
272
  });
264
273
 
265
- it('should handle network timeout during package validation', async () => {
266
- // Arrange
267
- const moduleName = 'salesforce';
268
-
269
- commandTester
270
- .mock('./install-command/validate-package', {
271
- validatePackageExists: jest.fn().mockRejectedValue(new Error('Network timeout'))
272
- })
273
- .mock('./install-command/logger', mocks.logger);
274
-
275
- // Act
276
- const result = await commandTester.execute([moduleName]);
277
-
278
- // Assert
279
- expect(result.success).toBe(false);
280
- expect(result.exitCode).toBe(1);
274
+ it('should sanitize label by removing spaces', async () => {
275
+ // Mock different package with spaces in label
276
+ searchAndSelectPackage.mockResolvedValue(['@friggframework/api-module-google-calendar']);
277
+
278
+ const path = require('path');
279
+ const googleCalendarPath = path.resolve(mockBackendPath, '../../node_modules/@friggframework/api-module-google-calendar');
280
+
281
+ jest.doMock(googleCalendarPath, () => ({
282
+ Config: { label: 'Google Calendar' }, // Has spaces
283
+ Api: class GoogleCalendarApi {}
284
+ }), { virtual: true });
285
+
286
+ await installCommand('google-calendar');
287
+
288
+ // Verify sanitized label used in file name (no spaces)
289
+ const writeCall = fs.writeFileSync.mock.calls.find(call =>
290
+ call[0].includes('Integration.js')
291
+ );
292
+
293
+ expect(writeCall[0]).toContain('GoogleCalendarIntegration.js');
294
+ expect(writeCall[0]).not.toContain(' ');
295
+
296
+ // Verify content uses sanitized name
297
+ expect(writeCall[1]).toContain('class GoogleCalendarIntegration');
298
+ expect(writeCall[1]).not.toMatch(/class Google Calendar/);
281
299
  });
300
+ });
301
+
302
+ describe('Early Exit Cases', () => {
303
+ it('should return early when no packages selected', async () => {
304
+ searchAndSelectPackage.mockResolvedValue([]);
282
305
 
283
- it('should handle permission denied during file operations', async () => {
284
- // Arrange
285
- const moduleName = 'salesforce';
286
-
287
- commandTester
288
- .mock('./install-command/validate-package', {
289
- validatePackageExists: jest.fn().mockResolvedValue(true)
290
- })
291
- .mock('@friggframework/core', {
292
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
293
- validateBackendPath: jest.fn().mockReturnValue(true)
294
- })
295
- .mock('./install-command/install-package', {
296
- installPackage: jest.fn().mockResolvedValue(true)
297
- })
298
- .mock('./install-command/integration-file', {
299
- createIntegrationFile: jest.fn().mockRejectedValue(new Error('EACCES: permission denied'))
300
- })
301
- .mock('./install-command/logger', mocks.logger);
302
-
303
- // Act
304
- const result = await commandTester.execute([moduleName]);
305
-
306
- // Assert
307
- expect(result.success).toBe(false);
308
- expect(result.exitCode).toBe(1);
306
+ await installCommand('slack');
307
+
308
+ expect(findNearestBackendPackageJson).not.toHaveBeenCalled();
309
+ expect(validatePackageExists).not.toHaveBeenCalled();
310
+ expect(installPackage).not.toHaveBeenCalled();
311
+ });
312
+
313
+ it('should return early when packages is null', async () => {
314
+ searchAndSelectPackage.mockResolvedValue(null);
315
+
316
+ await installCommand('slack');
317
+
318
+ expect(findNearestBackendPackageJson).not.toHaveBeenCalled();
319
+ });
320
+
321
+ it('should return early when packages is undefined', async () => {
322
+ searchAndSelectPackage.mockResolvedValue(undefined);
323
+
324
+ await installCommand('slack');
325
+
326
+ expect(findNearestBackendPackageJson).not.toHaveBeenCalled();
309
327
  });
310
328
  });
311
329
 
312
- describe('Option Validation', () => {
313
- it('should handle --app-path option', async () => {
314
- // Arrange
315
- const moduleName = 'salesforce';
316
- const customPath = '/custom/path';
317
-
318
- commandTester
319
- .mock('./install-command/validate-package', {
320
- validatePackageExists: jest.fn().mockResolvedValue(true)
321
- })
322
- .mock('@friggframework/core', {
323
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/custom/backend/package.json'),
324
- validateBackendPath: jest.fn().mockReturnValue(true)
325
- })
326
- .mock('./install-command/install-package', {
327
- installPackage: jest.fn().mockResolvedValue(true)
328
- })
329
- .mock('./install-command/integration-file', {
330
- createIntegrationFile: jest.fn().mockResolvedValue(true)
331
- })
332
- .mock('./install-command/backend-js', {
333
- updateBackendJsFile: jest.fn().mockResolvedValue(true)
334
- })
335
- .mock('./install-command/commit-changes', {
336
- commitChanges: jest.fn().mockResolvedValue(true)
337
- })
338
- .mock('./install-command/logger', mocks.logger);
339
-
340
- // Act
341
- const result = await commandTester.execute([moduleName, '--app-path', customPath]);
342
-
343
- // Assert
344
- expect(result.success).toBe(true);
345
- expect(result.args).toEqual([moduleName, '--app-path', customPath]);
330
+ describe('Error Handling', () => {
331
+ it('should log error and exit on searchAndSelectPackage failure', async () => {
332
+ const error = new Error('Search failed');
333
+ searchAndSelectPackage.mockRejectedValue(error);
334
+
335
+ await installCommand('slack');
336
+
337
+ // Verify error logged via console.error (we spy on it)
338
+ expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
339
+ expect(processExitSpy).toHaveBeenCalledWith(1);
346
340
  });
347
341
 
348
- it('should handle --config option', async () => {
349
- // Arrange
350
- const moduleName = 'salesforce';
351
- const configPath = '/custom/config.json';
352
-
353
- commandTester
354
- .mock('./install-command/validate-package', {
355
- validatePackageExists: jest.fn().mockResolvedValue(true)
356
- })
357
- .mock('@friggframework/core', {
358
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
359
- validateBackendPath: jest.fn().mockReturnValue(true)
360
- })
361
- .mock('./install-command/install-package', {
362
- installPackage: jest.fn().mockResolvedValue(true)
363
- })
364
- .mock('./install-command/integration-file', {
365
- createIntegrationFile: jest.fn().mockResolvedValue(true)
366
- })
367
- .mock('./install-command/backend-js', {
368
- updateBackendJsFile: jest.fn().mockResolvedValue(true)
369
- })
370
- .mock('./install-command/commit-changes', {
371
- commitChanges: jest.fn().mockResolvedValue(true)
372
- })
373
- .mock('./install-command/logger', mocks.logger);
374
-
375
- // Act
376
- const result = await commandTester.execute([moduleName, '--config', configPath]);
377
-
378
- // Assert
379
- expect(result.success).toBe(true);
380
- expect(result.args).toEqual([moduleName, '--config', configPath]);
342
+ it('should log error and exit on validatePackageExists failure', async () => {
343
+ const error = new Error('Package not found');
344
+ validatePackageExists.mockRejectedValue(error);
345
+
346
+ await installCommand('slack');
347
+
348
+ expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
349
+ expect(processExitSpy).toHaveBeenCalledWith(1);
381
350
  });
382
351
 
383
- it('should handle --app alias for --app-path', async () => {
384
- // Arrange
385
- const moduleName = 'salesforce';
386
- const customPath = '/custom/path';
387
-
388
- commandTester
389
- .mock('./install-command/validate-package', {
390
- validatePackageExists: jest.fn().mockResolvedValue(true)
391
- })
392
- .mock('@friggframework/core', {
393
- findNearestBackendPackageJson: jest.fn().mockReturnValue('/custom/backend/package.json'),
394
- validateBackendPath: jest.fn().mockReturnValue(true)
395
- })
396
- .mock('./install-command/install-package', {
397
- installPackage: jest.fn().mockResolvedValue(true)
398
- })
399
- .mock('./install-command/integration-file', {
400
- createIntegrationFile: jest.fn().mockResolvedValue(true)
401
- })
402
- .mock('./install-command/backend-js', {
403
- updateBackendJsFile: jest.fn().mockResolvedValue(true)
404
- })
405
- .mock('./install-command/commit-changes', {
406
- commitChanges: jest.fn().mockResolvedValue(true)
407
- })
408
- .mock('./install-command/logger', mocks.logger);
409
-
410
- // Act
411
- const result = await commandTester.execute([moduleName, '--app', customPath]);
412
-
413
- // Assert
414
- expect(result.success).toBe(true);
415
- expect(result.args).toEqual([moduleName, '--app', customPath]);
352
+ it('should log error and exit on installPackage failure', async () => {
353
+ const error = new Error('Installation failed');
354
+ installPackage.mockImplementation(() => {
355
+ throw error;
356
+ });
357
+
358
+ await installCommand('slack');
359
+
360
+ expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
361
+ expect(processExitSpy).toHaveBeenCalledWith(1);
362
+ });
363
+
364
+ it('should log error and exit on file write failure (createIntegrationFile)', async () => {
365
+ // Make fs.writeFileSync throw - tests REAL error path
366
+ const error = new Error('EACCES: permission denied');
367
+ fs.writeFileSync.mockImplementation(() => {
368
+ throw error;
369
+ });
370
+
371
+ await installCommand('slack');
372
+
373
+ expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
374
+ expect(processExitSpy).toHaveBeenCalledWith(1);
375
+ });
376
+
377
+ it('should log error and exit on backend.js read failure', async () => {
378
+ // Make fs.readFileSync throw - tests REAL error path
379
+ const error = new Error('ENOENT: file not found');
380
+ fs.readFileSync.mockImplementation(() => {
381
+ throw error;
382
+ });
383
+
384
+ await installCommand('slack');
385
+
386
+ expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
387
+ expect(processExitSpy).toHaveBeenCalledWith(1);
388
+ });
389
+
390
+ it('should log error and exit on handleEnvVariables failure', async () => {
391
+ const error = new Error('Env variables failed');
392
+ handleEnvVariables.mockRejectedValue(error);
393
+
394
+ await installCommand('slack');
395
+
396
+ expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
397
+ expect(processExitSpy).toHaveBeenCalledWith(1);
416
398
  });
417
399
  });
418
- });
400
+ });