@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.
- package/frigg-cli/README.md +1 -1
- package/frigg-cli/__tests__/application/use-cases/AddApiModuleToIntegrationUseCase.test.js +326 -0
- package/frigg-cli/__tests__/application/use-cases/CreateApiModuleUseCase.test.js +337 -0
- package/frigg-cli/__tests__/domain/entities/ApiModule.test.js +373 -0
- package/frigg-cli/__tests__/domain/entities/AppDefinition.test.js +313 -0
- package/frigg-cli/__tests__/domain/services/IntegrationValidator.test.js +269 -0
- package/frigg-cli/__tests__/domain/value-objects/IntegrationName.test.js +82 -0
- package/frigg-cli/__tests__/infrastructure/adapters/IntegrationJsUpdater.test.js +408 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemApiModuleRepository.test.js +583 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemAppDefinitionRepository.test.js +314 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemIntegrationRepository.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +1 -1
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +0 -2
- package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
- package/frigg-cli/__tests__/unit/commands/provider-dispatch.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
- package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
- package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
- package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
- package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
- package/frigg-cli/build-command/index.js +123 -11
- package/frigg-cli/container.js +172 -0
- package/frigg-cli/deploy-command/index.js +83 -1
- package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
- package/frigg-cli/doctor-command/index.js +37 -16
- package/frigg-cli/domain/entities/ApiModule.js +272 -0
- package/frigg-cli/domain/entities/AppDefinition.js +227 -0
- package/frigg-cli/domain/entities/Integration.js +198 -0
- package/frigg-cli/domain/exceptions/DomainException.js +24 -0
- package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
- package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
- package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
- package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
- package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
- package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
- package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
- package/frigg-cli/generate-iam-command.js +21 -1
- package/frigg-cli/index.js +21 -6
- package/frigg-cli/index.test.js +7 -2
- package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
- package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
- package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
- package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
- package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
- package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
- package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
- package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
- package/frigg-cli/init-command/backend-first-handler.js +124 -42
- package/frigg-cli/init-command/index.js +2 -1
- package/frigg-cli/init-command/template-handler.js +13 -3
- package/frigg-cli/install-command/backend-js.js +3 -3
- package/frigg-cli/install-command/environment-variables.js +16 -19
- package/frigg-cli/install-command/environment-variables.test.js +12 -13
- package/frigg-cli/install-command/index.js +14 -9
- package/frigg-cli/install-command/integration-file.js +3 -3
- package/frigg-cli/install-command/validate-package.js +5 -9
- package/frigg-cli/jest.config.js +4 -1
- package/frigg-cli/package-lock.json +16226 -0
- package/frigg-cli/repair-command/index.js +121 -128
- package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
- package/frigg-cli/start-command/index.js +324 -2
- package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
- package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
- package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
- package/frigg-cli/templates/backend/.env.example +62 -0
- package/frigg-cli/templates/backend/.eslintrc.json +12 -0
- package/frigg-cli/templates/backend/.prettierrc +6 -0
- package/frigg-cli/templates/backend/docker-compose.yml +22 -0
- package/frigg-cli/templates/backend/index.js +96 -0
- package/frigg-cli/templates/backend/infrastructure.js +12 -0
- package/frigg-cli/templates/backend/jest.config.js +17 -0
- package/frigg-cli/templates/backend/package.json +50 -0
- package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
- package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
- package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
- package/frigg-cli/templates/backend/test/setup.js +30 -0
- package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
- package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
- package/frigg-cli/ui-command/index.js +58 -36
- package/frigg-cli/utils/__tests__/provider-helper.test.js +55 -0
- package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
- package/frigg-cli/utils/output.js +382 -0
- package/frigg-cli/utils/provider-helper.js +75 -0
- package/frigg-cli/utils/repo-detection.js +85 -37
- package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
- package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
- package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
- package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
- package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
- package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
- package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
- package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +128 -0
- package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
- package/infrastructure/create-frigg-infrastructure.js +93 -0
- package/infrastructure/docs/iam-policy-templates.md +1 -1
- package/infrastructure/domains/admin-scripts/admin-script-builder.js +200 -0
- package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +499 -0
- package/infrastructure/domains/admin-scripts/index.js +5 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
- package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +4 -7
- package/infrastructure/domains/shared/resource-discovery.js +5 -5
- package/infrastructure/domains/shared/types/app-definition.js +21 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
- package/infrastructure/infrastructure-composer.js +2 -0
- package/infrastructure/infrastructure-composer.test.js +2 -2
- package/infrastructure/jest.config.js +16 -0
- package/management-ui/README.md +245 -109
- package/package.json +8 -7
- 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
|
+
});
|