@friggframework/devtools 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.0

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 (127) hide show
  1. package/frigg-cli/README.md +1 -1
  2. package/frigg-cli/__tests__/application/use-cases/AddApiModuleToIntegrationUseCase.test.js +326 -0
  3. package/frigg-cli/__tests__/application/use-cases/CreateApiModuleUseCase.test.js +337 -0
  4. package/frigg-cli/__tests__/domain/entities/ApiModule.test.js +373 -0
  5. package/frigg-cli/__tests__/domain/entities/AppDefinition.test.js +313 -0
  6. package/frigg-cli/__tests__/domain/services/IntegrationValidator.test.js +269 -0
  7. package/frigg-cli/__tests__/domain/value-objects/IntegrationName.test.js +82 -0
  8. package/frigg-cli/__tests__/infrastructure/adapters/IntegrationJsUpdater.test.js +408 -0
  9. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemApiModuleRepository.test.js +583 -0
  10. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemAppDefinitionRepository.test.js +314 -0
  11. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemIntegrationRepository.test.js +383 -0
  12. package/frigg-cli/__tests__/unit/commands/build.test.js +1 -1
  13. package/frigg-cli/__tests__/unit/commands/doctor.test.js +0 -2
  14. package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
  15. package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
  16. package/frigg-cli/__tests__/unit/commands/provider-dispatch.test.js +383 -0
  17. package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
  18. package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
  19. package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
  20. package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
  21. package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
  22. package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
  23. package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
  24. package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
  25. package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
  26. package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
  27. package/frigg-cli/build-command/index.js +123 -11
  28. package/frigg-cli/container.js +172 -0
  29. package/frigg-cli/deploy-command/index.js +83 -1
  30. package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
  31. package/frigg-cli/doctor-command/index.js +37 -16
  32. package/frigg-cli/domain/entities/ApiModule.js +272 -0
  33. package/frigg-cli/domain/entities/AppDefinition.js +227 -0
  34. package/frigg-cli/domain/entities/Integration.js +198 -0
  35. package/frigg-cli/domain/exceptions/DomainException.js +24 -0
  36. package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
  37. package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
  38. package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
  39. package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
  40. package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
  41. package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
  42. package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
  43. package/frigg-cli/generate-iam-command.js +21 -1
  44. package/frigg-cli/index.js +21 -6
  45. package/frigg-cli/index.test.js +7 -2
  46. package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
  47. package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
  48. package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
  49. package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
  50. package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
  51. package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
  52. package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
  53. package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
  54. package/frigg-cli/init-command/backend-first-handler.js +124 -42
  55. package/frigg-cli/init-command/index.js +2 -1
  56. package/frigg-cli/init-command/template-handler.js +13 -3
  57. package/frigg-cli/install-command/backend-js.js +3 -3
  58. package/frigg-cli/install-command/environment-variables.js +16 -19
  59. package/frigg-cli/install-command/environment-variables.test.js +12 -13
  60. package/frigg-cli/install-command/index.js +14 -9
  61. package/frigg-cli/install-command/integration-file.js +3 -3
  62. package/frigg-cli/install-command/validate-package.js +5 -9
  63. package/frigg-cli/jest.config.js +4 -1
  64. package/frigg-cli/package-lock.json +16226 -0
  65. package/frigg-cli/repair-command/index.js +121 -128
  66. package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
  67. package/frigg-cli/start-command/index.js +324 -2
  68. package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
  69. package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
  70. package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
  71. package/frigg-cli/templates/backend/.env.example +62 -0
  72. package/frigg-cli/templates/backend/.eslintrc.json +12 -0
  73. package/frigg-cli/templates/backend/.prettierrc +6 -0
  74. package/frigg-cli/templates/backend/docker-compose.yml +22 -0
  75. package/frigg-cli/templates/backend/index.js +96 -0
  76. package/frigg-cli/templates/backend/infrastructure.js +12 -0
  77. package/frigg-cli/templates/backend/jest.config.js +17 -0
  78. package/frigg-cli/templates/backend/package.json +50 -0
  79. package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
  80. package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
  81. package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
  82. package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
  83. package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
  84. package/frigg-cli/templates/backend/test/setup.js +30 -0
  85. package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
  86. package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
  87. package/frigg-cli/ui-command/index.js +58 -36
  88. package/frigg-cli/utils/__tests__/provider-helper.test.js +55 -0
  89. package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
  90. package/frigg-cli/utils/output.js +382 -0
  91. package/frigg-cli/utils/provider-helper.js +75 -0
  92. package/frigg-cli/utils/repo-detection.js +85 -37
  93. package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
  94. package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
  95. package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
  96. package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
  97. package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
  98. package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
  99. package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
  100. package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
  101. package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
  102. package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
  103. package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
  104. package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
  105. package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
  106. package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
  107. package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
  108. package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +128 -0
  109. package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
  110. package/infrastructure/create-frigg-infrastructure.js +93 -0
  111. package/infrastructure/docs/iam-policy-templates.md +1 -1
  112. package/infrastructure/domains/admin-scripts/admin-script-builder.js +200 -0
  113. package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +499 -0
  114. package/infrastructure/domains/admin-scripts/index.js +5 -0
  115. package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
  116. package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
  117. package/infrastructure/domains/shared/resource-discovery.js +5 -5
  118. package/infrastructure/domains/shared/types/app-definition.js +21 -0
  119. package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
  120. package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
  121. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
  122. package/infrastructure/infrastructure-composer.js +2 -0
  123. package/infrastructure/infrastructure-composer.test.js +2 -2
  124. package/infrastructure/jest.config.js +16 -0
  125. package/management-ui/README.md +245 -109
  126. package/package.json +8 -7
  127. package/frigg-cli/install-command/logger.js +0 -12
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Provider Dispatch Tests
3
+ *
4
+ * Verifies that CLI commands correctly route to provider plugins
5
+ * based on appDefinition.provider. These tests ensure:
6
+ *
7
+ * 1. AWS (default) falls through to existing serverless behavior
8
+ * 2. Non-AWS providers delegate to the provider plugin
9
+ * 3. AWS-only commands reject non-AWS providers
10
+ *
11
+ * When adding a new provider, these tests should pass without changes
12
+ * as long as the provider implements the plugin interface.
13
+ */
14
+
15
+ const path = require('path');
16
+
17
+ // Jest hoists jest.mock() calls — variable names prefixed with `mock` are allowed
18
+ function mockCreateProvider(overrides = {}) {
19
+ return {
20
+ name: 'test-provider',
21
+ deploy: jest.fn().mockResolvedValue({ url: 'https://test.example.com' }),
22
+ validate: jest.fn().mockReturnValue({ valid: true, errors: [], warnings: [] }),
23
+ preflightCheck: jest.fn().mockResolvedValue({ ready: true, missing: [] }),
24
+ generateConfig: jest.fn().mockReturnValue('# generated config'),
25
+ generateEnvTemplate: jest.fn().mockReturnValue({}),
26
+ getFunctionEntryPoints: jest.fn().mockReturnValue({
27
+ 'api.js': '// api handler',
28
+ 'worker-background.js': '// worker handler',
29
+ }),
30
+ detect: jest.fn().mockReturnValue(false),
31
+ createHandler: jest.fn(),
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Create a mock child process that auto-fires the 'close' event with exit 0.
38
+ * This prevents deploy command from hanging on the `await` for the close event.
39
+ */
40
+ function mockCreateChildProcess(exitCode = 0) {
41
+ return {
42
+ on: jest.fn((event, callback) => {
43
+ if (event === 'close') {
44
+ // Fire asynchronously to simulate real behavior
45
+ setImmediate(() => callback(exitCode));
46
+ }
47
+ }),
48
+ };
49
+ }
50
+
51
+ // ─── Deploy Command ────────────────────────────────────────────────
52
+
53
+ describe('deploy command: provider dispatch', () => {
54
+ let deployCommand;
55
+ let spawn;
56
+
57
+ beforeEach(() => {
58
+ jest.resetModules();
59
+ jest.clearAllMocks();
60
+
61
+ jest.mock('child_process', () => ({
62
+ spawn: jest.fn(),
63
+ }));
64
+
65
+ jest.mock('fs', () => ({
66
+ existsSync: jest.fn().mockReturnValue(false),
67
+ }));
68
+
69
+ // Mock doctor-command (deploy imports it; it has heavy AWS deps)
70
+ jest.mock('../../../doctor-command', () => ({
71
+ doctorCommand: jest.fn(),
72
+ }));
73
+
74
+ spawn = require('child_process').spawn;
75
+ spawn.mockReturnValue(mockCreateChildProcess(0));
76
+
77
+ jest.spyOn(console, 'log').mockImplementation();
78
+ jest.spyOn(console, 'warn').mockImplementation();
79
+ jest.spyOn(console, 'error').mockImplementation();
80
+ });
81
+
82
+ afterEach(() => {
83
+ jest.restoreAllMocks();
84
+ });
85
+
86
+ it('falls through to osls for AWS (no provider in appDefinition)', async () => {
87
+ ({ deployCommand } = require('../../../deploy-command'));
88
+
89
+ await deployCommand({ stage: 'dev', skipDoctor: true });
90
+
91
+ expect(spawn).toHaveBeenCalledWith(
92
+ 'osls',
93
+ expect.arrayContaining(['deploy']),
94
+ expect.any(Object)
95
+ );
96
+ });
97
+
98
+ it('falls through to osls when provider is explicitly "aws"', async () => {
99
+ const fs = require('fs');
100
+ fs.existsSync.mockReturnValue(true);
101
+
102
+ jest.mock(
103
+ path.join(process.cwd(), 'index.js'),
104
+ () => ({ Definition: { provider: 'aws', environment: {} } }),
105
+ { virtual: true }
106
+ );
107
+
108
+ ({ deployCommand } = require('../../../deploy-command'));
109
+ await deployCommand({ stage: 'dev', skipDoctor: true });
110
+
111
+ expect(spawn).toHaveBeenCalledWith(
112
+ 'osls',
113
+ expect.arrayContaining(['deploy']),
114
+ expect.any(Object)
115
+ );
116
+ });
117
+
118
+ it('delegates to provider.deploy() for non-AWS provider', async () => {
119
+ const mockProvider = mockCreateProvider({ name: 'netlify' });
120
+ const fs = require('fs');
121
+ fs.existsSync.mockReturnValue(true);
122
+
123
+ jest.mock(
124
+ path.join(process.cwd(), 'index.js'),
125
+ () => ({ Definition: { provider: 'netlify', environment: {} } }),
126
+ { virtual: true }
127
+ );
128
+
129
+ jest.mock('@friggframework/core/providers/resolve-provider', () => ({
130
+ resolveProvider: () => mockProvider,
131
+ }), { virtual: true });
132
+
133
+ ({ deployCommand } = require('../../../deploy-command'));
134
+ await deployCommand({ stage: 'production' });
135
+
136
+ // Should NOT spawn osls
137
+ expect(spawn).not.toHaveBeenCalled();
138
+
139
+ // Should call provider.validate then provider.deploy
140
+ expect(mockProvider.validate).toHaveBeenCalled();
141
+ expect(mockProvider.deploy).toHaveBeenCalledWith(
142
+ expect.objectContaining({ provider: 'netlify' }),
143
+ expect.objectContaining({ stage: 'production', prod: true })
144
+ );
145
+ });
146
+
147
+ it('exits when provider.validate() reports errors', async () => {
148
+ const mockProvider = mockCreateProvider({
149
+ name: 'netlify',
150
+ validate: jest.fn().mockReturnValue({
151
+ valid: false,
152
+ errors: ['KMS encryption not supported on Netlify'],
153
+ warnings: [],
154
+ }),
155
+ });
156
+
157
+ const fs = require('fs');
158
+ fs.existsSync.mockReturnValue(true);
159
+
160
+ jest.mock(
161
+ path.join(process.cwd(), 'index.js'),
162
+ () => ({ Definition: { provider: 'netlify' } }),
163
+ { virtual: true }
164
+ );
165
+
166
+ jest.mock('@friggframework/core/providers/resolve-provider', () => ({
167
+ resolveProvider: () => mockProvider,
168
+ }), { virtual: true });
169
+
170
+ jest.spyOn(process, 'exit').mockImplementation();
171
+
172
+ ({ deployCommand } = require('../../../deploy-command'));
173
+ await deployCommand({ stage: 'dev' });
174
+
175
+ expect(process.exit).toHaveBeenCalledWith(1);
176
+ expect(console.error).toHaveBeenCalledWith(
177
+ expect.stringContaining('KMS encryption not supported')
178
+ );
179
+ });
180
+
181
+ it('exits when provider.deploy() throws', async () => {
182
+ const mockDeployError = new Error('Preflight check failed');
183
+ mockDeployError.missing = ['NETLIFY_AUTH_TOKEN'];
184
+
185
+ const mockProvider = mockCreateProvider({
186
+ name: 'netlify',
187
+ deploy: jest.fn().mockRejectedValue(mockDeployError),
188
+ });
189
+
190
+ const fs = require('fs');
191
+ fs.existsSync.mockReturnValue(true);
192
+
193
+ jest.mock(
194
+ path.join(process.cwd(), 'index.js'),
195
+ () => ({ Definition: { provider: 'netlify' } }),
196
+ { virtual: true }
197
+ );
198
+
199
+ jest.mock('@friggframework/core/providers/resolve-provider', () => ({
200
+ resolveProvider: () => mockProvider,
201
+ }), { virtual: true });
202
+
203
+ jest.spyOn(process, 'exit').mockImplementation();
204
+
205
+ ({ deployCommand } = require('../../../deploy-command'));
206
+ await deployCommand({ stage: 'dev' });
207
+
208
+ expect(process.exit).toHaveBeenCalledWith(1);
209
+ expect(console.error).toHaveBeenCalledWith(
210
+ expect.stringContaining('Preflight check failed')
211
+ );
212
+ });
213
+ });
214
+
215
+ // ─── Build Command ─────────────────────────────────────────────────
216
+
217
+ describe('build command: provider dispatch', () => {
218
+ let buildCommand;
219
+ let spawnSync;
220
+
221
+ beforeEach(() => {
222
+ jest.resetModules();
223
+ jest.clearAllMocks();
224
+
225
+ jest.mock('child_process', () => ({
226
+ spawnSync: jest.fn().mockReturnValue({ status: 0 }),
227
+ }));
228
+
229
+ spawnSync = require('child_process').spawnSync;
230
+
231
+ jest.spyOn(console, 'log').mockImplementation();
232
+ jest.spyOn(console, 'warn').mockImplementation();
233
+ jest.spyOn(console, 'error').mockImplementation();
234
+ jest.spyOn(process, 'exit').mockImplementation();
235
+ });
236
+
237
+ afterEach(() => {
238
+ jest.restoreAllMocks();
239
+ });
240
+
241
+ it('falls through to osls for AWS (default)', async () => {
242
+ ({ buildCommand } = require('../../../build-command'));
243
+
244
+ await buildCommand({ stage: 'dev' });
245
+
246
+ expect(spawnSync).toHaveBeenCalledWith(
247
+ 'osls',
248
+ expect.arrayContaining(['package']),
249
+ expect.any(Object)
250
+ );
251
+ });
252
+
253
+ it('delegates to provider build for non-AWS', async () => {
254
+ const mockProvider = mockCreateProvider({ name: 'netlify' });
255
+
256
+ jest.mock('../../../utils/provider-helper', () => ({
257
+ loadProviderForCli: () => ({
258
+ appDefinition: { provider: 'netlify' },
259
+ provider: mockProvider,
260
+ providerName: 'netlify',
261
+ }),
262
+ }));
263
+
264
+ jest.mock('fs', () => ({
265
+ writeFileSync: jest.fn(),
266
+ mkdirSync: jest.fn(),
267
+ }));
268
+
269
+ ({ buildCommand } = require('../../../build-command'));
270
+ await buildCommand({ stage: 'dev' });
271
+
272
+ // Should NOT call osls
273
+ expect(spawnSync).not.toHaveBeenCalled();
274
+
275
+ // Should call provider's build pipeline
276
+ expect(mockProvider.validate).toHaveBeenCalled();
277
+ expect(mockProvider.generateConfig).toHaveBeenCalled();
278
+ expect(mockProvider.getFunctionEntryPoints).toHaveBeenCalled();
279
+ });
280
+ });
281
+
282
+ // ─── AWS-Only Command Guards ───────────────────────────────────────
283
+
284
+ describe('AWS-only command guards', () => {
285
+ let processExitSpy;
286
+
287
+ beforeEach(() => {
288
+ jest.resetModules();
289
+ jest.clearAllMocks();
290
+
291
+ jest.spyOn(console, 'log').mockImplementation();
292
+ jest.spyOn(console, 'warn').mockImplementation();
293
+ jest.spyOn(console, 'error').mockImplementation();
294
+ processExitSpy = jest.spyOn(process, 'exit').mockImplementation();
295
+ });
296
+
297
+ afterEach(() => {
298
+ jest.restoreAllMocks();
299
+ });
300
+
301
+ it('repair command rejects non-AWS provider', async () => {
302
+ jest.mock('../../../utils/provider-helper', () => ({
303
+ loadProviderForCli: () => ({
304
+ appDefinition: { provider: 'netlify' },
305
+ provider: mockCreateProvider({ name: 'netlify' }),
306
+ providerName: 'netlify',
307
+ }),
308
+ }));
309
+
310
+ jest.mock('../../../utils/output', () => ({
311
+ info: jest.fn(),
312
+ error: jest.fn(),
313
+ log: jest.fn(),
314
+ success: jest.fn(),
315
+ warn: jest.fn(),
316
+ }));
317
+
318
+ jest.mock('../../../../infrastructure/domains/health/domain/value-objects/stack-identifier', () => jest.fn());
319
+ jest.mock('../../../../infrastructure/domains/health/application/use-cases/run-health-check-use-case', () => jest.fn());
320
+ jest.mock('../../../../infrastructure/domains/health/application/use-cases/repair-via-import-use-case', () => jest.fn());
321
+ jest.mock('../../../../infrastructure/domains/health/application/use-cases/reconcile-properties-use-case', () => jest.fn());
322
+ jest.mock('../../../../infrastructure/domains/health/application/use-cases/execute-resource-import-use-case', () => jest.fn());
323
+ jest.mock('../../../../infrastructure/domains/health/infrastructure/adapters/aws-stack-repository', () => jest.fn());
324
+ jest.mock('../../../../infrastructure/domains/health/infrastructure/adapters/aws-resource-detector', () => jest.fn());
325
+ jest.mock('../../../../infrastructure/domains/health/infrastructure/adapters/aws-resource-importer', () => jest.fn());
326
+ jest.mock('../../../../infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler', () => jest.fn());
327
+ jest.mock('../../../../infrastructure/domains/health/domain/services/mismatch-analyzer', () => jest.fn());
328
+ jest.mock('../../../../infrastructure/domains/health/domain/services/health-score-calculator', () => jest.fn());
329
+ jest.mock('../../../../infrastructure/domains/health/domain/services/template-parser', () => ({
330
+ TemplateParser: jest.fn(),
331
+ }));
332
+ jest.mock('../../../../infrastructure/domains/health/domain/services/import-template-generator', () => ({
333
+ ImportTemplateGenerator: jest.fn(),
334
+ }));
335
+ jest.mock('../../../../infrastructure/domains/health/domain/services/import-progress-monitor', () => ({
336
+ ImportProgressMonitor: jest.fn(),
337
+ }));
338
+
339
+ const { repairCommand } = require('../../../repair-command');
340
+ const output = require('../../../utils/output');
341
+
342
+ await repairCommand('my-stack', { import: true });
343
+
344
+ expect(output.error).toHaveBeenCalledWith(
345
+ expect.stringContaining('only available for AWS')
346
+ );
347
+ expect(processExitSpy).toHaveBeenCalledWith(1);
348
+ });
349
+
350
+ it('generate-iam command rejects non-AWS provider', async () => {
351
+ jest.mock('../../../utils/provider-helper', () => ({
352
+ loadProviderForCli: () => ({
353
+ appDefinition: { provider: 'netlify' },
354
+ provider: mockCreateProvider({ name: 'netlify' }),
355
+ providerName: 'netlify',
356
+ }),
357
+ }));
358
+
359
+ jest.mock('fs-extra', () => ({
360
+ existsSync: jest.fn(),
361
+ writeFileSync: jest.fn(),
362
+ ensureDirSync: jest.fn(),
363
+ }), { virtual: true });
364
+
365
+ jest.mock('@friggframework/core', () => ({
366
+ findNearestBackendPackageJson: jest.fn(),
367
+ }), { virtual: true });
368
+
369
+ jest.mock('../../../../infrastructure/domains/security/iam-generator', () => ({
370
+ generateIAMCloudFormation: jest.fn(),
371
+ getFeatureSummary: jest.fn(),
372
+ }));
373
+
374
+ const { generateIamCommand } = require('../../../generate-iam-command');
375
+
376
+ await generateIamCommand({});
377
+
378
+ expect(processExitSpy).toHaveBeenCalledWith(1);
379
+ expect(console.error).toHaveBeenCalledWith(
380
+ expect.stringContaining('only available for AWS')
381
+ );
382
+ });
383
+ });
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Unit tests for frigg repair command
3
+ * Tests repair workflow orchestration and output usage
4
+ */
5
+
6
+ // Mock all external dependencies
7
+ jest.mock('../../../utils/output');
8
+ jest.mock('../../../repair-command/index.js', () => {
9
+ const actualModule = jest.requireActual('../../../repair-command/index.js');
10
+ return actualModule;
11
+ }, { virtual: false });
12
+
13
+ const output = require('../../../utils/output');
14
+
15
+ describe('Repair Command - Output Integration', () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+
19
+ // Setup output mocks
20
+ output.success = jest.fn();
21
+ output.error = jest.fn();
22
+ output.info = jest.fn();
23
+ output.warn = jest.fn();
24
+ output.log = jest.fn();
25
+ output.confirm = jest.fn();
26
+ });
27
+
28
+ describe('Output method usage', () => {
29
+ test('should use output.success for successful operations', () => {
30
+ // Test that success messages use output.success
31
+ output.success(' No orphaned resources to import');
32
+
33
+ expect(output.success).toHaveBeenCalledWith(
34
+ expect.stringContaining('No orphaned resources')
35
+ );
36
+ });
37
+
38
+ test('should use output.error for error messages', () => {
39
+ // Test that errors use output.error
40
+ const error = new Error('Stack not found');
41
+ output.error('An error occurred:', error);
42
+
43
+ expect(output.error).toHaveBeenCalledWith('An error occurred:', error);
44
+ });
45
+
46
+ test('should use output.info for informational messages', () => {
47
+ // Test that info messages use output.info with emoji
48
+ output.info('🔍 Analyzing stack health...');
49
+
50
+ expect(output.info).toHaveBeenCalledWith(
51
+ expect.stringContaining('Analyzing stack')
52
+ );
53
+ });
54
+
55
+ test('should use output.warn for warnings', () => {
56
+ // Test that warnings use output.warn
57
+ output.warn('️ Build template not found');
58
+
59
+ expect(output.warn).toHaveBeenCalledWith(
60
+ expect.stringContaining('Build template not found')
61
+ );
62
+ });
63
+
64
+ test('should use output.log for general messages', () => {
65
+ // Test that general messages use output.log
66
+ output.log(' • serverless package');
67
+
68
+ expect(output.log).toHaveBeenCalledWith(
69
+ expect.stringContaining('serverless package')
70
+ );
71
+ });
72
+
73
+ test('should use output.confirm for user confirmations', async () => {
74
+ // Test that confirmations use output.confirm
75
+ output.confirm.mockResolvedValue(true);
76
+
77
+ const result = await output.confirm('Import 5 orphaned resource(s)?');
78
+
79
+ expect(output.confirm).toHaveBeenCalledWith(
80
+ expect.stringContaining('Import')
81
+ );
82
+ expect(result).toBe(true);
83
+ });
84
+ });
85
+
86
+ describe('Error handling scenarios', () => {
87
+ test('should handle and report stack not found errors', () => {
88
+ const error = new Error('Stack does not exist');
89
+ output.error('An error occurred:', error);
90
+
91
+ expect(output.error).toHaveBeenCalledWith('An error occurred:', error);
92
+ });
93
+
94
+ test('should handle and report import validation errors', () => {
95
+ output.log('\nValidation errors:');
96
+ output.log(' • Resource1: Invalid property');
97
+
98
+ expect(output.log).toHaveBeenCalledWith('\nValidation errors:');
99
+ expect(output.log).toHaveBeenCalledWith(expect.stringContaining('Invalid property'));
100
+ });
101
+
102
+ test('should handle and report AWS API errors', () => {
103
+ const error = new Error('AccessDenied: Insufficient permissions');
104
+ output.error('An error occurred:', error);
105
+
106
+ expect(output.error).toHaveBeenCalledWith('An error occurred:', error);
107
+ });
108
+ });
109
+
110
+ describe('User workflow scenarios', () => {
111
+ test('should report when no orphaned resources are found', () => {
112
+ output.success(' No orphaned resources to import');
113
+
114
+ expect(output.success).toHaveBeenCalled();
115
+ expect(output.success).toHaveBeenCalledWith(
116
+ expect.stringContaining('No orphaned resources')
117
+ );
118
+ });
119
+
120
+ test('should list orphaned resources before import', () => {
121
+ output.info('📦 Found 3 orphaned resource(s) to import:');
122
+ output.log(' 1. AWS::Lambda::Function - my-function');
123
+ output.log(' 2. AWS::S3::Bucket - my-bucket');
124
+ output.log(' 3. AWS::DynamoDB::Table - my-table');
125
+
126
+ expect(output.info).toHaveBeenCalledWith(
127
+ expect.stringMatching(/Found \d+ orphaned/)
128
+ );
129
+ expect(output.log).toHaveBeenCalledTimes(3);
130
+ });
131
+
132
+ test('should warn when build template is missing', () => {
133
+ output.warn('️ Build template not found. Generating sequential logical IDs (not recommended).');
134
+ output.log(' Run one of the following to generate build template:');
135
+ output.log(' • serverless package');
136
+
137
+ expect(output.warn).toHaveBeenCalledWith(
138
+ expect.stringContaining('Build template not found')
139
+ );
140
+ expect(output.log).toHaveBeenCalledWith(
141
+ expect.stringContaining('serverless package')
142
+ );
143
+ });
144
+
145
+ test('should confirm before performing import', async () => {
146
+ output.confirm.mockResolvedValue(true);
147
+
148
+ const confirmed = await output.confirm('Import 5 orphaned resource(s) with sequential IDs?');
149
+
150
+ expect(output.confirm).toHaveBeenCalled();
151
+ expect(confirmed).toBe(true);
152
+ });
153
+
154
+ test('should handle user cancellation gracefully', async () => {
155
+ output.confirm.mockResolvedValue(false);
156
+
157
+ const confirmed = await output.confirm('Import resources?');
158
+ if (!confirmed) {
159
+ output.log('Import cancelled');
160
+ }
161
+
162
+ expect(output.confirm).toHaveBeenCalled();
163
+ expect(output.log).toHaveBeenCalledWith('Import cancelled');
164
+ });
165
+
166
+ test('should report successful import results', () => {
167
+ output.success(' Successfully imported 5 resource(s)');
168
+
169
+ expect(output.success).toHaveBeenCalledWith(
170
+ expect.stringMatching(/Successfully imported \d+/)
171
+ );
172
+ });
173
+ });
174
+
175
+ describe('Repair workflow stages', () => {
176
+ test('should progress through health check stage', () => {
177
+ output.info('🏥 Running health check on stack...');
178
+
179
+ expect(output.info).toHaveBeenCalledWith(
180
+ expect.stringContaining('health check')
181
+ );
182
+ });
183
+
184
+ test('should progress through import stage', () => {
185
+ output.info('🔧 Importing resources with sequential IDs...');
186
+
187
+ expect(output.info).toHaveBeenCalledWith(
188
+ expect.stringContaining('Importing resources')
189
+ );
190
+ });
191
+
192
+ test('should progress through reconciliation stage', () => {
193
+ output.info('🔄 Reconciling property drift...');
194
+
195
+ expect(output.info).toHaveBeenCalledWith(
196
+ expect.stringContaining('Reconciling')
197
+ );
198
+ });
199
+
200
+ test('should report completion with summary', () => {
201
+ output.success(' Repair completed successfully');
202
+ output.log('\nSummary:');
203
+ output.log(' • Imported: 5 resources');
204
+ output.log(' • Reconciled: 3 properties');
205
+
206
+ expect(output.success).toHaveBeenCalledWith(
207
+ expect.stringContaining('completed successfully')
208
+ );
209
+ expect(output.log).toHaveBeenCalledWith('\nSummary:');
210
+ });
211
+ });
212
+
213
+ describe('Output consistency', () => {
214
+ test('should not use console.log directly', () => {
215
+ // Verify that all logging goes through output module
216
+ const consoleLogSpy = jest.spyOn(console, 'log');
217
+
218
+ output.log('Test message');
219
+
220
+ // output.log may call console.log internally, but command code shouldn't
221
+ expect(output.log).toHaveBeenCalled();
222
+
223
+ consoleLogSpy.mockRestore();
224
+ });
225
+
226
+ test('should not use console.error directly', () => {
227
+ // Verify that all errors go through output module
228
+ const consoleErrorSpy = jest.spyOn(console, 'error');
229
+
230
+ const error = new Error('Test error');
231
+ output.error('An error occurred:', error);
232
+
233
+ // output.error may call console.error internally, but command code shouldn't
234
+ expect(output.error).toHaveBeenCalled();
235
+
236
+ consoleErrorSpy.mockRestore();
237
+ });
238
+
239
+ test('should use consistent emoji patterns', () => {
240
+ // Verify emoji usage follows patterns
241
+ output.success(' Success message'); // ✓ or ✅
242
+ output.error(' Error message'); // ✗ or ❌
243
+ output.warn('️ Warning message'); // ⚠️
244
+ output.info('🔍 Info message'); // Various info emojis
245
+
246
+ expect(output.success).toHaveBeenCalled();
247
+ expect(output.error).toHaveBeenCalled();
248
+ expect(output.warn).toHaveBeenCalled();
249
+ expect(output.info).toHaveBeenCalled();
250
+ });
251
+ });
252
+
253
+ describe('Migration verification', () => {
254
+ test('should have migrated all console.log calls', () => {
255
+ // This test verifies the migration was complete
256
+ // In the actual command file, there should be 0 console.log references
257
+ expect(output.log).toBeDefined();
258
+ expect(output.info).toBeDefined();
259
+ expect(output.success).toBeDefined();
260
+ });
261
+
262
+ test('should have migrated all console.error calls', () => {
263
+ // This test verifies the migration was complete
264
+ // In the actual command file, there should be 0 console.error references
265
+ expect(output.error).toBeDefined();
266
+ expect(output.warn).toBeDefined();
267
+ });
268
+
269
+ test('should have migrated readline confirm to output.confirm', () => {
270
+ // This test verifies readline was replaced with output.confirm
271
+ expect(output.confirm).toBeDefined();
272
+ expect(typeof output.confirm).toBe('function');
273
+ });
274
+ });
275
+ });
@@ -51,8 +51,8 @@ describe('frigg-cli dependencies', () => {
51
51
  expect(packageJson.dependencies.commander).toBeDefined();
52
52
  });
53
53
 
54
- it('should have @friggframework/devtools for infrastructure', () => {
55
- expect(packageJson.dependencies['@friggframework/devtools']).toBeDefined();
54
+ it('should have @friggframework/devtools as a peer dependency', () => {
55
+ expect(packageJson.peerDependencies['@friggframework/devtools']).toBeDefined();
56
56
  });
57
57
  });
58
58