@friggframework/devtools 2.0.0--canary.546.74db90f.0 → 2.0.0--canary.545.e7becd9.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 (128) 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/cloudformation-discovery.test.js +4 -7
  118. package/infrastructure/domains/shared/resource-discovery.js +5 -5
  119. package/infrastructure/domains/shared/types/app-definition.js +21 -0
  120. package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
  121. package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
  122. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
  123. package/infrastructure/infrastructure-composer.js +2 -0
  124. package/infrastructure/infrastructure-composer.test.js +2 -2
  125. package/infrastructure/jest.config.js +16 -0
  126. package/management-ui/README.md +245 -109
  127. package/package.json +8 -7
  128. package/frigg-cli/install-command/logger.js +0 -12
@@ -0,0 +1,474 @@
1
+ /**
2
+ * InteractivePromptAdapter Tests
3
+ * Handles prompts in terminal mode (inquirer) or IPC mode (JSON over stdio)
4
+ *
5
+ * Tests follow TDD pattern - written BEFORE implementation
6
+ */
7
+
8
+ // Mock @inquirer/prompts
9
+ jest.mock('@inquirer/prompts', () => ({
10
+ confirm: jest.fn(),
11
+ select: jest.fn(),
12
+ input: jest.fn()
13
+ }));
14
+
15
+ const { confirm, select, input } = require('@inquirer/prompts');
16
+
17
+ // Import after mocks
18
+ const {
19
+ InteractivePromptAdapter,
20
+ TerminalPromptAdapter,
21
+ IpcPromptAdapter
22
+ } = require('../../../../start-command/presentation/InteractivePromptAdapter');
23
+
24
+ describe('InteractivePromptAdapter', () => {
25
+ describe('factory method - create()', () => {
26
+ it('should create TerminalPromptAdapter when mode is terminal', () => {
27
+ const adapter = InteractivePromptAdapter.create({ mode: 'terminal' });
28
+ expect(adapter).toBeInstanceOf(TerminalPromptAdapter);
29
+ });
30
+
31
+ it('should create IpcPromptAdapter when mode is ipc', () => {
32
+ const adapter = InteractivePromptAdapter.create({ mode: 'ipc' });
33
+ expect(adapter).toBeInstanceOf(IpcPromptAdapter);
34
+ });
35
+
36
+ it('should default to terminal mode when no mode specified', () => {
37
+ const adapter = InteractivePromptAdapter.create({});
38
+ expect(adapter).toBeInstanceOf(TerminalPromptAdapter);
39
+ });
40
+
41
+ it('should use ipc mode when FRIGG_IPC env var is true', () => {
42
+ const originalEnv = process.env.FRIGG_IPC;
43
+ process.env.FRIGG_IPC = 'true';
44
+
45
+ const adapter = InteractivePromptAdapter.create({});
46
+ expect(adapter).toBeInstanceOf(IpcPromptAdapter);
47
+
48
+ process.env.FRIGG_IPC = originalEnv;
49
+ });
50
+ });
51
+ });
52
+
53
+ describe('TerminalPromptAdapter', () => {
54
+ let adapter;
55
+
56
+ beforeEach(() => {
57
+ jest.clearAllMocks();
58
+ adapter = new TerminalPromptAdapter();
59
+ });
60
+
61
+ describe('confirm()', () => {
62
+ it('should call inquirer confirm with message', async () => {
63
+ confirm.mockResolvedValue(true);
64
+
65
+ const result = await adapter.confirm({
66
+ message: 'Start Docker Desktop?',
67
+ default: true
68
+ });
69
+
70
+ expect(result).toBe(true);
71
+ expect(confirm).toHaveBeenCalledWith({
72
+ message: 'Start Docker Desktop?',
73
+ default: true
74
+ });
75
+ });
76
+
77
+ it('should return false when user declines', async () => {
78
+ confirm.mockResolvedValue(false);
79
+
80
+ const result = await adapter.confirm({
81
+ message: 'Continue?'
82
+ });
83
+
84
+ expect(result).toBe(false);
85
+ });
86
+
87
+ it('should use default value when provided', async () => {
88
+ confirm.mockResolvedValue(false);
89
+
90
+ await adapter.confirm({
91
+ message: 'Continue?',
92
+ default: false
93
+ });
94
+
95
+ expect(confirm).toHaveBeenCalledWith(expect.objectContaining({
96
+ default: false
97
+ }));
98
+ });
99
+ });
100
+
101
+ describe('select()', () => {
102
+ it('should call inquirer select with options', async () => {
103
+ select.mockResolvedValue('option1');
104
+
105
+ const result = await adapter.select({
106
+ message: 'Choose an option:',
107
+ choices: [
108
+ { value: 'option1', name: 'Option 1' },
109
+ { value: 'option2', name: 'Option 2' }
110
+ ]
111
+ });
112
+
113
+ expect(result).toBe('option1');
114
+ expect(select).toHaveBeenCalledWith(expect.objectContaining({
115
+ message: 'Choose an option:'
116
+ }));
117
+ });
118
+
119
+ it('should return selected value', async () => {
120
+ select.mockResolvedValue('option2');
121
+
122
+ const result = await adapter.select({
123
+ message: 'Choose:',
124
+ choices: [
125
+ { value: 'option1', name: 'Option 1' },
126
+ { value: 'option2', name: 'Option 2' }
127
+ ]
128
+ });
129
+
130
+ expect(result).toBe('option2');
131
+ });
132
+ });
133
+
134
+ describe('input()', () => {
135
+ it('should call inquirer input with message', async () => {
136
+ input.mockResolvedValue('user input');
137
+
138
+ const result = await adapter.input({
139
+ message: 'Enter value:'
140
+ });
141
+
142
+ expect(result).toBe('user input');
143
+ expect(input).toHaveBeenCalledWith(expect.objectContaining({
144
+ message: 'Enter value:'
145
+ }));
146
+ });
147
+
148
+ it('should use default value when provided', async () => {
149
+ input.mockResolvedValue('default');
150
+
151
+ await adapter.input({
152
+ message: 'Enter value:',
153
+ default: 'default'
154
+ });
155
+
156
+ expect(input).toHaveBeenCalledWith(expect.objectContaining({
157
+ default: 'default'
158
+ }));
159
+ });
160
+ });
161
+
162
+ describe('promptForResolution()', () => {
163
+ it('should prompt confirm for start_docker resolution', async () => {
164
+ confirm.mockResolvedValue(true);
165
+
166
+ const result = await adapter.promptForResolution({
167
+ name: 'docker_running',
168
+ status: 'failed',
169
+ message: 'Docker is not running',
170
+ canResolve: true,
171
+ resolution: {
172
+ type: 'start_docker',
173
+ prompt: 'Would you like to start Docker Desktop?'
174
+ }
175
+ });
176
+
177
+ expect(result.shouldResolve).toBe(true);
178
+ expect(confirm).toHaveBeenCalledWith(expect.objectContaining({
179
+ message: 'Would you like to start Docker Desktop?'
180
+ }));
181
+ });
182
+
183
+ it('should prompt confirm for start_docker_compose resolution', async () => {
184
+ confirm.mockResolvedValue(true);
185
+
186
+ const result = await adapter.promptForResolution({
187
+ name: 'database_reachable',
188
+ status: 'failed',
189
+ message: 'Database not reachable',
190
+ canResolve: true,
191
+ resolution: {
192
+ type: 'start_docker_compose',
193
+ prompt: 'Would you like to run docker-compose up?',
194
+ composePath: '/test/docker-compose.yml'
195
+ }
196
+ });
197
+
198
+ expect(result.shouldResolve).toBe(true);
199
+ expect(result.composePath).toBe('/test/docker-compose.yml');
200
+ });
201
+
202
+ it('should return shouldResolve: false when user declines', async () => {
203
+ confirm.mockResolvedValue(false);
204
+
205
+ const result = await adapter.promptForResolution({
206
+ name: 'docker_running',
207
+ status: 'failed',
208
+ message: 'Docker not running',
209
+ canResolve: true,
210
+ resolution: {
211
+ type: 'start_docker',
212
+ prompt: 'Start Docker?'
213
+ }
214
+ });
215
+
216
+ expect(result.shouldResolve).toBe(false);
217
+ });
218
+
219
+ it('should return shouldResolve: false for non-resolvable checks', async () => {
220
+ const result = await adapter.promptForResolution({
221
+ name: 'docker_installed',
222
+ status: 'failed',
223
+ message: 'Docker not installed',
224
+ canResolve: false,
225
+ resolution: {
226
+ type: 'manual',
227
+ instructions: 'Install Docker manually'
228
+ }
229
+ });
230
+
231
+ expect(result.shouldResolve).toBe(false);
232
+ expect(confirm).not.toHaveBeenCalled();
233
+ });
234
+ });
235
+ });
236
+
237
+ describe('IpcPromptAdapter', () => {
238
+ let adapter;
239
+ let originalStdout;
240
+ let mockStdout;
241
+
242
+ beforeEach(() => {
243
+ jest.clearAllMocks();
244
+ adapter = new IpcPromptAdapter();
245
+
246
+ // Mock stdout.write
247
+ mockStdout = jest.fn();
248
+ originalStdout = process.stdout.write;
249
+ process.stdout.write = mockStdout;
250
+ });
251
+
252
+ afterEach(() => {
253
+ process.stdout.write = originalStdout;
254
+ });
255
+
256
+ describe('confirm()', () => {
257
+ it('should output JSON prompt request to stdout', async () => {
258
+ // Mock requestId generation for predictable test
259
+ adapter._generateRequestId = () => 'test-id';
260
+
261
+ const resultPromise = adapter.confirm({
262
+ message: 'Start Docker?',
263
+ default: true
264
+ });
265
+
266
+ // Resolve immediately for test
267
+ adapter._resolvePrompt('test-id', true);
268
+
269
+ const result = await resultPromise;
270
+
271
+ expect(result).toBe(true);
272
+ expect(mockStdout).toHaveBeenCalledWith(expect.stringContaining('frigg_ipc'));
273
+ expect(mockStdout).toHaveBeenCalledWith(expect.stringContaining('prompt_request'));
274
+ expect(mockStdout).toHaveBeenCalledWith(expect.stringContaining('confirm'));
275
+ });
276
+
277
+ it('should include requestId in output', async () => {
278
+ adapter._generateRequestId = () => 'unique-id-123';
279
+ adapter._resolvePrompt = jest.fn();
280
+
281
+ // Start the promise but don't await yet
282
+ adapter.confirm({ message: 'Test?' });
283
+
284
+ // Give time for stdout.write to be called
285
+ await new Promise(resolve => setTimeout(resolve, 10));
286
+
287
+ const output = mockStdout.mock.calls[0][0];
288
+ expect(output).toContain('unique-id-123');
289
+ });
290
+
291
+ it('should output newline-terminated JSON', async () => {
292
+ adapter._generateRequestId = () => 'test-id';
293
+
294
+ adapter.confirm({ message: 'Test?' });
295
+
296
+ await new Promise(resolve => setTimeout(resolve, 10));
297
+
298
+ const output = mockStdout.mock.calls[0][0];
299
+ expect(output.endsWith('\n')).toBe(true);
300
+ });
301
+ });
302
+
303
+ describe('_parseIpcMessage()', () => {
304
+ it('should parse valid prompt response', () => {
305
+ const message = JSON.stringify({
306
+ frigg_ipc: 'prompt_response',
307
+ requestId: 'test-id',
308
+ response: true
309
+ });
310
+
311
+ const result = adapter._parseIpcMessage(message);
312
+
313
+ expect(result.type).toBe('prompt_response');
314
+ expect(result.requestId).toBe('test-id');
315
+ expect(result.response).toBe(true);
316
+ });
317
+
318
+ it('should return null for non-IPC messages', () => {
319
+ const message = 'regular log message';
320
+ const result = adapter._parseIpcMessage(message);
321
+
322
+ expect(result).toBeNull();
323
+ });
324
+
325
+ it('should return null for invalid JSON', () => {
326
+ const message = '{ invalid json }';
327
+ const result = adapter._parseIpcMessage(message);
328
+
329
+ expect(result).toBeNull();
330
+ });
331
+ });
332
+
333
+ describe('_formatIpcOutput()', () => {
334
+ it('should format prompt request as JSON', () => {
335
+ const output = adapter._formatIpcOutput('prompt_request', {
336
+ requestId: 'test-123',
337
+ prompt: {
338
+ type: 'confirm',
339
+ message: 'Continue?',
340
+ default: true
341
+ }
342
+ });
343
+
344
+ const parsed = JSON.parse(output.trim());
345
+ expect(parsed.frigg_ipc).toBe('prompt_request');
346
+ expect(parsed.requestId).toBe('test-123');
347
+ expect(parsed.prompt.type).toBe('confirm');
348
+ });
349
+
350
+ it('should add newline to output', () => {
351
+ const output = adapter._formatIpcOutput('prompt_request', {});
352
+ expect(output.endsWith('\n')).toBe(true);
353
+ });
354
+ });
355
+
356
+ describe('handleResponse()', () => {
357
+ it('should resolve pending prompt with response', async () => {
358
+ adapter._generateRequestId = () => 'test-id';
359
+
360
+ const resultPromise = adapter.confirm({ message: 'Test?' });
361
+
362
+ // Wait for prompt to be registered
363
+ await new Promise(resolve => setTimeout(resolve, 10));
364
+
365
+ // Handle the response
366
+ adapter.handleResponse('test-id', true);
367
+
368
+ const result = await resultPromise;
369
+ expect(result).toBe(true);
370
+ });
371
+
372
+ it('should ignore responses for unknown requestIds', () => {
373
+ // Should not throw
374
+ expect(() => {
375
+ adapter.handleResponse('unknown-id', true);
376
+ }).not.toThrow();
377
+ });
378
+ });
379
+
380
+ describe('promptForResolution()', () => {
381
+ it('should output prompt in IPC format', async () => {
382
+ adapter._generateRequestId = () => 'test-id';
383
+
384
+ const check = {
385
+ name: 'docker_running',
386
+ status: 'failed',
387
+ message: 'Docker not running',
388
+ canResolve: true,
389
+ resolution: {
390
+ type: 'start_docker',
391
+ prompt: 'Start Docker Desktop?'
392
+ }
393
+ };
394
+
395
+ const resultPromise = adapter.promptForResolution(check);
396
+
397
+ await new Promise(resolve => setTimeout(resolve, 10));
398
+
399
+ const output = mockStdout.mock.calls[0][0];
400
+ expect(output).toContain('prompt_request');
401
+ expect(output).toContain('Start Docker Desktop?');
402
+
403
+ // Resolve to complete the test
404
+ adapter.handleResponse('test-id', true);
405
+ await resultPromise;
406
+ });
407
+ });
408
+ });
409
+
410
+ describe('IPC Protocol Format', () => {
411
+ describe('prompt_request format', () => {
412
+ it('should match expected IPC protocol for confirm prompts', () => {
413
+ const adapter = new IpcPromptAdapter();
414
+ const output = adapter._formatIpcOutput('prompt_request', {
415
+ requestId: 'prompt-1234',
416
+ prompt: {
417
+ type: 'confirm',
418
+ message: 'Docker is not running. Start Docker Desktop?',
419
+ default: true
420
+ }
421
+ });
422
+
423
+ const parsed = JSON.parse(output.trim());
424
+
425
+ expect(parsed).toEqual({
426
+ frigg_ipc: 'prompt_request',
427
+ requestId: 'prompt-1234',
428
+ prompt: {
429
+ type: 'confirm',
430
+ message: 'Docker is not running. Start Docker Desktop?',
431
+ default: true
432
+ }
433
+ });
434
+ });
435
+
436
+ it('should match expected IPC protocol for select prompts', () => {
437
+ const adapter = new IpcPromptAdapter();
438
+ const output = adapter._formatIpcOutput('prompt_request', {
439
+ requestId: 'prompt-5678',
440
+ prompt: {
441
+ type: 'select',
442
+ message: 'Choose an action:',
443
+ choices: [
444
+ { value: 'start', name: 'Start services' },
445
+ { value: 'skip', name: 'Skip' }
446
+ ]
447
+ }
448
+ });
449
+
450
+ const parsed = JSON.parse(output.trim());
451
+
452
+ expect(parsed.frigg_ipc).toBe('prompt_request');
453
+ expect(parsed.prompt.type).toBe('select');
454
+ expect(parsed.prompt.choices).toHaveLength(2);
455
+ });
456
+ });
457
+
458
+ describe('prompt_response format', () => {
459
+ it('should parse prompt_response messages correctly', () => {
460
+ const adapter = new IpcPromptAdapter();
461
+ const response = JSON.stringify({
462
+ frigg_ipc: 'prompt_response',
463
+ requestId: 'prompt-1234',
464
+ response: true
465
+ });
466
+
467
+ const parsed = adapter._parseIpcMessage(response);
468
+
469
+ expect(parsed.type).toBe('prompt_response');
470
+ expect(parsed.requestId).toBe('prompt-1234');
471
+ expect(parsed.response).toBe(true);
472
+ });
473
+ });
474
+ });
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Tests for unified Output utility
3
+ */
4
+
5
+ const output = require('../../../utils/output');
6
+
7
+ describe('Output Utility', () => {
8
+ // Mock console methods
9
+ let originalConsole;
10
+
11
+ beforeEach(() => {
12
+ originalConsole = { ...console };
13
+ console.log = jest.fn();
14
+ console.error = jest.fn();
15
+ console.warn = jest.fn();
16
+ });
17
+
18
+ afterEach(() => {
19
+ console.log = originalConsole.log;
20
+ console.error = originalConsole.error;
21
+ console.warn = originalConsole.warn;
22
+ });
23
+
24
+ describe('success()', () => {
25
+ it('should display success message with checkmark', () => {
26
+ output.success('Operation completed');
27
+ expect(console.log).toHaveBeenCalled();
28
+ const args = console.log.mock.calls[0];
29
+ expect(args.join(' ')).toContain('Operation completed');
30
+ });
31
+ });
32
+
33
+ describe('error()', () => {
34
+ it('should display error message with X mark', () => {
35
+ output.error('Operation failed');
36
+ expect(console.error).toHaveBeenCalled();
37
+ const args = console.error.mock.calls[0];
38
+ expect(args.join(' ')).toContain('Operation failed');
39
+ });
40
+
41
+ it('should display error stack in debug mode', () => {
42
+ const originalDebug = process.env.DEBUG;
43
+ process.env.DEBUG = 'true';
44
+
45
+ const error = new Error('Test error');
46
+ output.error('Operation failed', error);
47
+
48
+ expect(console.error).toHaveBeenCalledTimes(2);
49
+
50
+ process.env.DEBUG = originalDebug;
51
+ });
52
+ });
53
+
54
+ describe('info()', () => {
55
+ it('should display info message', () => {
56
+ output.info('Information message');
57
+ expect(console.log).toHaveBeenCalled();
58
+ const args = console.log.mock.calls[0];
59
+ expect(args.join(' ')).toContain('Information message');
60
+ });
61
+ });
62
+
63
+ describe('warn()', () => {
64
+ it('should display warning message', () => {
65
+ output.warn('Warning message');
66
+ expect(console.warn).toHaveBeenCalled();
67
+ const args = console.warn.mock.calls[0];
68
+ expect(args.join(' ')).toContain('Warning message');
69
+ });
70
+ });
71
+
72
+ describe('header()', () => {
73
+ it('should display formatted header', () => {
74
+ output.header('Test Header');
75
+ expect(console.log).toHaveBeenCalledTimes(3); // empty line, title, separator
76
+ });
77
+ });
78
+
79
+ describe('table()', () => {
80
+ it('should display table with data', () => {
81
+ const data = [
82
+ { name: 'Module A', version: '1.0.0', status: 'active' },
83
+ { name: 'Module B', version: '2.1.0', status: 'inactive' }
84
+ ];
85
+
86
+ output.table(data);
87
+ expect(console.log).toHaveBeenCalled();
88
+ expect(console.log.mock.calls.length).toBeGreaterThan(3); // header + separator + rows
89
+ });
90
+
91
+ it('should handle empty data', () => {
92
+ output.table([]);
93
+ expect(console.log).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('No data to display'));
94
+ });
95
+
96
+ it('should handle specific columns', () => {
97
+ const data = [
98
+ { name: 'Module A', version: '1.0.0', status: 'active', extra: 'ignored' }
99
+ ];
100
+
101
+ output.table(data, ['name', 'version']);
102
+ expect(console.log).toHaveBeenCalled();
103
+ });
104
+ });
105
+
106
+ describe('keyValue()', () => {
107
+ it('should display key-value pairs', () => {
108
+ const data = {
109
+ 'Module Name': 'test-module',
110
+ 'Version': '1.0.0',
111
+ 'Status': 'active'
112
+ };
113
+
114
+ output.keyValue(data);
115
+ expect(console.log).toHaveBeenCalledTimes(3);
116
+ });
117
+ });
118
+
119
+ describe('json()', () => {
120
+ it('should display formatted JSON', () => {
121
+ const data = { name: 'test', version: '1.0.0', active: true };
122
+
123
+ output.json(data);
124
+ expect(console.log).toHaveBeenCalled();
125
+ const output_text = console.log.mock.calls[0][0];
126
+ expect(output_text).toContain('name');
127
+ expect(output_text).toContain('1.0.0');
128
+ });
129
+ });
130
+
131
+ describe('spinner()', () => {
132
+ jest.useFakeTimers();
133
+
134
+ it('should create and control spinner', () => {
135
+ const spinner = output.spinner('Loading...');
136
+
137
+ // Spinner should have control methods
138
+ expect(spinner).toHaveProperty('update');
139
+ expect(spinner).toHaveProperty('succeed');
140
+ expect(spinner).toHaveProperty('fail');
141
+ expect(spinner).toHaveProperty('stop');
142
+
143
+ spinner.stop();
144
+ });
145
+
146
+ it('should succeed with message', () => {
147
+ const spinner = output.spinner('Loading...');
148
+ spinner.succeed('Loaded successfully');
149
+
150
+ expect(console.log).toHaveBeenCalled();
151
+ });
152
+
153
+ it('should fail with message', () => {
154
+ const spinner = output.spinner('Loading...');
155
+ spinner.fail('Loading failed');
156
+
157
+ expect(console.error).toHaveBeenCalled();
158
+ });
159
+
160
+ jest.useRealTimers();
161
+ });
162
+
163
+ describe('progress()', () => {
164
+ let originalStdout;
165
+
166
+ beforeEach(() => {
167
+ originalStdout = process.stdout.write;
168
+ process.stdout.write = jest.fn();
169
+ });
170
+
171
+ afterEach(() => {
172
+ process.stdout.write = originalStdout;
173
+ });
174
+
175
+ it('should display progress bar', () => {
176
+ output.progress(50, 100, 'Processing...');
177
+ expect(process.stdout.write).toHaveBeenCalled();
178
+
179
+ const output_text = process.stdout.write.mock.calls[0][0];
180
+ expect(output_text).toContain('%');
181
+ expect(output_text).toContain('Processing...');
182
+ });
183
+
184
+ it('should complete progress bar', () => {
185
+ output.progress(100, 100);
186
+ expect(console.log).toHaveBeenCalled(); // Newline on completion
187
+ });
188
+ });
189
+
190
+ describe('log()', () => {
191
+ it('should log raw messages', () => {
192
+ output.log('Raw message');
193
+ expect(console.log).toHaveBeenCalledWith('Raw message');
194
+ });
195
+ });
196
+ });