@onlineapps/conn-orch-validator 2.0.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 (37) hide show
  1. package/README.md +78 -0
  2. package/TESTING_STRATEGY.md +92 -0
  3. package/docs/DESIGN.md +134 -0
  4. package/examples/service-wrapper-usage.js +250 -0
  5. package/examples/three-tier-testing.js +144 -0
  6. package/jest.config.js +23 -0
  7. package/onlineapps-conn-e2e-testing-1.0.0.tgz +0 -0
  8. package/package.json +43 -0
  9. package/src/CookbookTestRunner.js +434 -0
  10. package/src/CookbookTestUtils.js +237 -0
  11. package/src/ServiceReadinessValidator.js +430 -0
  12. package/src/ServiceTestHarness.js +256 -0
  13. package/src/ServiceValidator.js +387 -0
  14. package/src/TestOrchestrator.js +727 -0
  15. package/src/ValidationOrchestrator.js +506 -0
  16. package/src/WorkflowTestRunner.js +396 -0
  17. package/src/helpers/README.md +235 -0
  18. package/src/helpers/createPreValidationTests.js +321 -0
  19. package/src/helpers/createServiceReadinessTests.js +245 -0
  20. package/src/index.js +62 -0
  21. package/src/mocks/MockMQClient.js +176 -0
  22. package/src/mocks/MockRegistry.js +164 -0
  23. package/src/mocks/MockStorage.js +186 -0
  24. package/src/validators/ServiceStructureValidator.js +487 -0
  25. package/src/validators/ValidationProofGenerator.js +79 -0
  26. package/test-mq-flow.js +72 -0
  27. package/test-orchestrator.js +95 -0
  28. package/tests/component/testing-framework-integration.test.js +313 -0
  29. package/tests/integration/ServiceReadiness.test.js +265 -0
  30. package/tests/monitoring-e2e.test.js +315 -0
  31. package/tests/run-example.js +257 -0
  32. package/tests/unit/CookbookTestRunner.test.js +353 -0
  33. package/tests/unit/MockMQClient.test.js +190 -0
  34. package/tests/unit/MockRegistry.test.js +233 -0
  35. package/tests/unit/MockStorage.test.js +257 -0
  36. package/tests/unit/ServiceValidator.test.js +429 -0
  37. package/tests/unit/WorkflowTestRunner.test.js +546 -0
@@ -0,0 +1,727 @@
1
+ 'use strict';
2
+
3
+ const ServiceValidator = require('./ServiceValidator');
4
+ const WorkflowTestRunner = require('./WorkflowTestRunner');
5
+ const ServiceTestHarness = require('./ServiceTestHarness');
6
+ const ServiceReadinessValidator = require('./ServiceReadinessValidator');
7
+
8
+ /**
9
+ * TestOrchestrator - Three-tier testing strategy
10
+ * Level 1: Unit tests with full mocks
11
+ * Level 2: Component tests with partial mocks
12
+ * Level 3: Integration tests with real services
13
+ */
14
+ class TestOrchestrator {
15
+ constructor(options = {}) {
16
+ this.service = options.service;
17
+ this.serviceName = options.serviceName;
18
+ this.openApiSpec = options.openApiSpec;
19
+ this.logger = options.logger || console;
20
+
21
+ // Test configuration
22
+ this.testLevels = {
23
+ unit: {
24
+ useMocks: true,
25
+ useTestQueues: false,
26
+ useRealServices: false
27
+ },
28
+ component: {
29
+ useMocks: true,
30
+ useTestQueues: true,
31
+ useRealServices: false
32
+ },
33
+ integration: {
34
+ useMocks: false,
35
+ useTestQueues: true,
36
+ useRealServices: true
37
+ }
38
+ };
39
+
40
+ // Test suites
41
+ this.testSuites = {
42
+ unit: [],
43
+ component: [],
44
+ integration: []
45
+ };
46
+
47
+ // Results
48
+ this.results = {
49
+ unit: null,
50
+ component: null,
51
+ integration: null
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Run all test levels
57
+ */
58
+ async runAllTests() {
59
+ const results = {
60
+ timestamp: Date.now(),
61
+ service: this.serviceName,
62
+ levels: {}
63
+ };
64
+
65
+ // Level 1: Unit Tests
66
+ this.logger.info('Starting Level 1: Unit Tests (full mocks)...');
67
+ results.levels.unit = await this.runUnitTests();
68
+
69
+ if (!results.levels.unit.passed) {
70
+ results.recommendation = 'Failed at unit test level - fix basic functionality';
71
+ return results;
72
+ }
73
+
74
+ // Level 2: Component Tests
75
+ this.logger.info('Starting Level 2: Component Tests (partial mocks + test queues)...');
76
+ results.levels.component = await this.runComponentTests();
77
+
78
+ if (!results.levels.component.passed) {
79
+ results.recommendation = 'Failed at component test level - fix integration issues';
80
+ return results;
81
+ }
82
+
83
+ // Level 3: Integration Tests
84
+ this.logger.info('Starting Level 3: Integration Tests (real services + test queues)...');
85
+ results.levels.integration = await this.runIntegrationTests();
86
+
87
+ if (!results.levels.integration.passed) {
88
+ results.recommendation = 'Failed at integration level - service not production ready';
89
+ return results;
90
+ }
91
+
92
+ results.passed = true;
93
+ results.recommendation = 'All test levels passed - service is production ready';
94
+ return results;
95
+ }
96
+
97
+ /**
98
+ * Level 1: Unit Tests with full mocks
99
+ */
100
+ async runUnitTests() {
101
+ const results = {
102
+ level: 'unit',
103
+ passed: true,
104
+ tests: [],
105
+ coverage: 0
106
+ };
107
+
108
+ try {
109
+ // Create test harness with full mocks
110
+ const harness = new ServiceTestHarness({
111
+ service: this.service,
112
+ serviceName: this.serviceName,
113
+ openApiSpec: this.openApiSpec,
114
+ mockInfrastructure: true
115
+ });
116
+
117
+ await harness.start();
118
+
119
+ // Test 1: Service starts and responds
120
+ const startTest = await this.testServiceStart(harness);
121
+ results.tests.push(startTest);
122
+ if (!startTest.passed) results.passed = false;
123
+
124
+ // Test 2: API endpoints exist
125
+ const endpointTest = await this.testEndpointsExist(harness);
126
+ results.tests.push(endpointTest);
127
+ if (!endpointTest.passed) results.passed = false;
128
+
129
+ // Test 3: Health check works
130
+ const healthTest = await this.testHealthCheck(harness);
131
+ results.tests.push(healthTest);
132
+ if (!healthTest.passed) results.passed = false;
133
+
134
+ // Test 4: Can handle mock messages
135
+ const messageTest = await this.testMockMessageHandling(harness);
136
+ results.tests.push(messageTest);
137
+ if (!messageTest.passed) results.passed = false;
138
+
139
+ await harness.stop();
140
+
141
+ // Calculate coverage
142
+ const passedTests = results.tests.filter(t => t.passed).length;
143
+ results.coverage = (passedTests / results.tests.length) * 100;
144
+
145
+ } catch (error) {
146
+ results.passed = false;
147
+ results.error = error.message;
148
+ }
149
+
150
+ return results;
151
+ }
152
+
153
+ /**
154
+ * Level 2: Component Tests with test queues
155
+ */
156
+ async runComponentTests() {
157
+ const results = {
158
+ level: 'component',
159
+ passed: true,
160
+ tests: [],
161
+ coverage: 0
162
+ };
163
+
164
+ try {
165
+ // Create test harness with test queues
166
+ const harness = new ServiceTestHarness({
167
+ service: this.service,
168
+ serviceName: this.serviceName,
169
+ openApiSpec: this.openApiSpec,
170
+ mockInfrastructure: true // Still use mocks but with test queue names
171
+ });
172
+
173
+ // Configure test queues
174
+ this.configureTestQueues(harness);
175
+
176
+ await harness.start();
177
+
178
+ // Test 1: OpenAPI compliance
179
+ const openApiTest = await this.testOpenApiCompliance(harness);
180
+ results.tests.push(openApiTest);
181
+ if (!openApiTest.passed) results.passed = false;
182
+
183
+ // Test 2: Workflow execution
184
+ const workflowTest = await this.testWorkflowExecution(harness);
185
+ results.tests.push(workflowTest);
186
+ if (!workflowTest.passed) results.passed = false;
187
+
188
+ // Test 3: Message routing
189
+ const routingTest = await this.testMessageRouting(harness);
190
+ results.tests.push(routingTest);
191
+ if (!routingTest.passed) results.passed = false;
192
+
193
+ // Test 4: Error handling
194
+ const errorTest = await this.testErrorHandling(harness);
195
+ results.tests.push(errorTest);
196
+ if (!errorTest.passed) results.passed = false;
197
+
198
+ await harness.stop();
199
+
200
+ // Calculate coverage
201
+ const passedTests = results.tests.filter(t => t.passed).length;
202
+ results.coverage = (passedTests / results.tests.length) * 100;
203
+
204
+ } catch (error) {
205
+ results.passed = false;
206
+ results.error = error.message;
207
+ }
208
+
209
+ return results;
210
+ }
211
+
212
+ /**
213
+ * Level 3: Integration Tests with real services
214
+ */
215
+ async runIntegrationTests() {
216
+ const results = {
217
+ level: 'integration',
218
+ passed: true,
219
+ tests: [],
220
+ coverage: 0
221
+ };
222
+
223
+ try {
224
+ // Use real infrastructure with test configuration
225
+ const config = this.getIntegrationTestConfig();
226
+
227
+ // Run readiness validator (uses real endpoints)
228
+ const validator = new ServiceReadinessValidator();
229
+ const readinessTest = await validator.validateReadiness({
230
+ name: this.serviceName,
231
+ url: config.serviceUrl,
232
+ openApiSpec: this.openApiSpec,
233
+ registry: config.registry,
234
+ testCookbook: this.getTestCookbook(),
235
+ healthEndpoint: '/health'
236
+ });
237
+
238
+ results.tests.push({
239
+ name: 'Service Readiness',
240
+ passed: readinessTest.ready,
241
+ score: readinessTest.score,
242
+ details: readinessTest
243
+ });
244
+
245
+ if (!readinessTest.ready) {
246
+ results.passed = false;
247
+ }
248
+
249
+ // Test with real message queue (test exchange)
250
+ if (config.mqUrl) {
251
+ const mqTest = await this.testRealMessageQueue(config);
252
+ results.tests.push(mqTest);
253
+ if (!mqTest.passed) results.passed = false;
254
+ }
255
+
256
+ // Test with real registry (test environment)
257
+ if (config.registryUrl) {
258
+ const registryTest = await this.testRealRegistry(config);
259
+ results.tests.push(registryTest);
260
+ if (!registryTest.passed) results.passed = false;
261
+ }
262
+
263
+ // Test end-to-end workflow
264
+ const e2eTest = await this.testEndToEndWorkflow(config);
265
+ results.tests.push(e2eTest);
266
+ if (!e2eTest.passed) results.passed = false;
267
+
268
+ // Calculate coverage
269
+ const passedTests = results.tests.filter(t => t.passed).length;
270
+ results.coverage = (passedTests / results.tests.length) * 100;
271
+
272
+ } catch (error) {
273
+ results.passed = false;
274
+ results.error = error.message;
275
+ }
276
+
277
+ return results;
278
+ }
279
+
280
+ // === Unit Test Methods ===
281
+
282
+ async testServiceStart(harness) {
283
+ try {
284
+ const isRunning = harness.isRunning;
285
+ return {
286
+ name: 'Service Start',
287
+ passed: isRunning,
288
+ message: isRunning ? 'Service started successfully' : 'Service failed to start'
289
+ };
290
+ } catch (error) {
291
+ return {
292
+ name: 'Service Start',
293
+ passed: false,
294
+ error: error.message
295
+ };
296
+ }
297
+ }
298
+
299
+ async testEndpointsExist(harness) {
300
+ try {
301
+ const endpoints = this.extractEndpoints(this.openApiSpec);
302
+ const tested = [];
303
+
304
+ for (const endpoint of endpoints.slice(0, 3)) { // Test first 3
305
+ try {
306
+ await harness.callApi(endpoint.method, endpoint.path);
307
+ tested.push({ path: endpoint.path, exists: true });
308
+ } catch {
309
+ tested.push({ path: endpoint.path, exists: false });
310
+ }
311
+ }
312
+
313
+ const passed = tested.every(t => t.exists);
314
+ return {
315
+ name: 'Endpoints Exist',
316
+ passed,
317
+ tested,
318
+ message: `${tested.filter(t => t.exists).length}/${tested.length} endpoints exist`
319
+ };
320
+ } catch (error) {
321
+ return {
322
+ name: 'Endpoints Exist',
323
+ passed: false,
324
+ error: error.message
325
+ };
326
+ }
327
+ }
328
+
329
+ async testHealthCheck(harness) {
330
+ try {
331
+ const response = await harness.callApi('GET', '/health');
332
+ const passed = response && response.status === 'healthy';
333
+ return {
334
+ name: 'Health Check',
335
+ passed,
336
+ response,
337
+ message: passed ? 'Health check passed' : 'Health check failed'
338
+ };
339
+ } catch (error) {
340
+ return {
341
+ name: 'Health Check',
342
+ passed: false,
343
+ error: error.message
344
+ };
345
+ }
346
+ }
347
+
348
+ async testMockMessageHandling(harness) {
349
+ try {
350
+ const message = {
351
+ workflow_id: 'test-123',
352
+ step: 'test-step',
353
+ data: { test: true }
354
+ };
355
+
356
+ await harness.mqClient.publish('test-queue', message);
357
+ const published = harness.getPublishedMessages('test-queue');
358
+
359
+ const passed = published.length > 0;
360
+ return {
361
+ name: 'Mock Message Handling',
362
+ passed,
363
+ messagesHandled: published.length,
364
+ message: `Handled ${published.length} messages`
365
+ };
366
+ } catch (error) {
367
+ return {
368
+ name: 'Mock Message Handling',
369
+ passed: false,
370
+ error: error.message
371
+ };
372
+ }
373
+ }
374
+
375
+ // === Component Test Methods ===
376
+
377
+ async testOpenApiCompliance(harness) {
378
+ try {
379
+ const validator = new ServiceValidator();
380
+ const result = await validator.validateService(
381
+ harness.baseUrl,
382
+ this.openApiSpec
383
+ );
384
+
385
+ return {
386
+ name: 'OpenAPI Compliance',
387
+ passed: result.valid,
388
+ coverage: result.coverage,
389
+ errors: result.errors,
390
+ message: `${result.coverage}% endpoint coverage`
391
+ };
392
+ } catch (error) {
393
+ return {
394
+ name: 'OpenAPI Compliance',
395
+ passed: false,
396
+ error: error.message
397
+ };
398
+ }
399
+ }
400
+
401
+ async testWorkflowExecution(harness) {
402
+ try {
403
+ const cookbook = this.getTestCookbook();
404
+ const result = await harness.simulateWorkflow(cookbook);
405
+
406
+ return {
407
+ name: 'Workflow Execution',
408
+ passed: result.completed,
409
+ stepsExecuted: result.results.length,
410
+ message: `Executed ${result.results.length} workflow steps`
411
+ };
412
+ } catch (error) {
413
+ return {
414
+ name: 'Workflow Execution',
415
+ passed: false,
416
+ error: error.message
417
+ };
418
+ }
419
+ }
420
+
421
+ async testMessageRouting(harness) {
422
+ try {
423
+ // Publish to different queues
424
+ await harness.mqClient.publish('test.service.request', { type: 'request' });
425
+ await harness.mqClient.publish('test.service.response', { type: 'response' });
426
+ await harness.mqClient.publish('test.workflow.event', { type: 'event' });
427
+
428
+ const requests = harness.getPublishedMessages('test.service.request');
429
+ const responses = harness.getPublishedMessages('test.service.response');
430
+ const events = harness.getPublishedMessages('test.workflow.event');
431
+
432
+ const passed = requests.length > 0 && responses.length > 0 && events.length > 0;
433
+
434
+ return {
435
+ name: 'Message Routing',
436
+ passed,
437
+ queues: {
438
+ requests: requests.length,
439
+ responses: responses.length,
440
+ events: events.length
441
+ },
442
+ message: 'Message routing working correctly'
443
+ };
444
+ } catch (error) {
445
+ return {
446
+ name: 'Message Routing',
447
+ passed: false,
448
+ error: error.message
449
+ };
450
+ }
451
+ }
452
+
453
+ async testErrorHandling(harness) {
454
+ try {
455
+ // Test various error scenarios
456
+ const errors = [];
457
+
458
+ // Invalid endpoint
459
+ try {
460
+ await harness.callApi('GET', '/non-existent');
461
+ } catch (e) {
462
+ errors.push('Handled 404 correctly');
463
+ }
464
+
465
+ // Invalid payload
466
+ try {
467
+ await harness.callApi('POST', '/api/endpoint', 'invalid-json');
468
+ } catch (e) {
469
+ errors.push('Handled invalid JSON');
470
+ }
471
+
472
+ const passed = errors.length >= 1;
473
+
474
+ return {
475
+ name: 'Error Handling',
476
+ passed,
477
+ errorsHandled: errors.length,
478
+ details: errors,
479
+ message: `Handled ${errors.length} error scenarios`
480
+ };
481
+ } catch (error) {
482
+ return {
483
+ name: 'Error Handling',
484
+ passed: false,
485
+ error: error.message
486
+ };
487
+ }
488
+ }
489
+
490
+ // === Integration Test Methods ===
491
+
492
+ async testRealMessageQueue(config) {
493
+ try {
494
+ const MQClient = require('@onlineapps/connector-mq-client');
495
+ const mqClient = new MQClient({
496
+ url: config.mqUrl,
497
+ queue: `test.${this.serviceName}`,
498
+ exchange: 'test.exchange'
499
+ });
500
+
501
+ await mqClient.connect();
502
+
503
+ const testMessage = {
504
+ test: true,
505
+ timestamp: Date.now(),
506
+ service: this.serviceName
507
+ };
508
+
509
+ await mqClient.publish(`test.${this.serviceName}`, testMessage);
510
+
511
+ // Set up consumer to verify
512
+ let received = false;
513
+ await mqClient.consume(`test.${this.serviceName}`, (msg) => {
514
+ const content = JSON.parse(msg.content.toString());
515
+ if (content.test && content.service === this.serviceName) {
516
+ received = true;
517
+ }
518
+ mqClient.ack(msg);
519
+ });
520
+
521
+ // Wait for message
522
+ await new Promise(resolve => setTimeout(resolve, 1000));
523
+
524
+ await mqClient.disconnect();
525
+
526
+ return {
527
+ name: 'Real Message Queue',
528
+ passed: received,
529
+ message: received ? 'Successfully sent and received test message' : 'Message not received'
530
+ };
531
+ } catch (error) {
532
+ return {
533
+ name: 'Real Message Queue',
534
+ passed: false,
535
+ error: error.message
536
+ };
537
+ }
538
+ }
539
+
540
+ async testRealRegistry(config) {
541
+ try {
542
+ // Test registration with real registry
543
+ const response = await require('axios').post(
544
+ `${config.registryUrl}/register`,
545
+ {
546
+ name: `test-${this.serviceName}`,
547
+ version: '1.0.0-test',
548
+ url: config.serviceUrl,
549
+ openapi: this.openApiSpec
550
+ }
551
+ );
552
+
553
+ const passed = response.status === 200;
554
+
555
+ // Clean up test registration
556
+ if (passed) {
557
+ await require('axios').delete(
558
+ `${config.registryUrl}/unregister/test-${this.serviceName}`
559
+ );
560
+ }
561
+
562
+ return {
563
+ name: 'Real Registry',
564
+ passed,
565
+ message: 'Successfully registered and unregistered with real registry'
566
+ };
567
+ } catch (error) {
568
+ return {
569
+ name: 'Real Registry',
570
+ passed: false,
571
+ error: error.message
572
+ };
573
+ }
574
+ }
575
+
576
+ async testEndToEndWorkflow(config) {
577
+ try {
578
+ // Create real workflow runner
579
+ const runner = new WorkflowTestRunner({
580
+ mqClient: config.mqClient,
581
+ registry: config.registry,
582
+ timeout: 30000
583
+ });
584
+
585
+ const cookbook = this.getTestCookbook();
586
+ const workflow = await runner.runWorkflow(cookbook, {
587
+ test: true,
588
+ service: this.serviceName
589
+ });
590
+
591
+ const passed = workflow.status === 'completed';
592
+
593
+ return {
594
+ name: 'End-to-End Workflow',
595
+ passed,
596
+ workflowId: workflow.id,
597
+ status: workflow.status,
598
+ steps: workflow.results.length,
599
+ message: `Workflow ${workflow.status} with ${workflow.results.length} steps`
600
+ };
601
+ } catch (error) {
602
+ return {
603
+ name: 'End-to-End Workflow',
604
+ passed: false,
605
+ error: error.message
606
+ };
607
+ }
608
+ }
609
+
610
+ // === Helper Methods ===
611
+
612
+ configureTestQueues(harness) {
613
+ // Configure test queue names
614
+ const testQueues = {
615
+ request: `test.${this.serviceName}.request`,
616
+ response: `test.${this.serviceName}.response`,
617
+ workflow: `test.workflow.${this.serviceName}`,
618
+ dlq: `test.${this.serviceName}.dlq`
619
+ };
620
+
621
+ // Apply to mock MQ client
622
+ if (harness.mqClient) {
623
+ harness.mqClient.testQueues = testQueues;
624
+ }
625
+
626
+ return testQueues;
627
+ }
628
+
629
+ getIntegrationTestConfig() {
630
+ // Get configuration for integration tests
631
+ return {
632
+ serviceUrl: process.env.TEST_SERVICE_URL || `http://localhost:${process.env.PORT || 3000}`,
633
+ mqUrl: process.env.TEST_MQ_URL || process.env.RABBITMQ_URL,
634
+ registryUrl: process.env.TEST_REGISTRY_URL || 'http://localhost:8080',
635
+ storageUrl: process.env.TEST_STORAGE_URL || process.env.MINIO_URL,
636
+ registry: null, // Would be real registry instance
637
+ mqClient: null // Would be real MQ client instance
638
+ };
639
+ }
640
+
641
+ getTestCookbook() {
642
+ // Generate test cookbook based on service OpenAPI
643
+ const steps = [];
644
+
645
+ if (this.openApiSpec?.paths) {
646
+ const endpoints = this.extractEndpoints(this.openApiSpec);
647
+
648
+ // Add first few endpoints as test steps
649
+ endpoints.slice(0, 3).forEach((endpoint, index) => {
650
+ steps.push({
651
+ id: `test-step-${index}`,
652
+ type: 'task',
653
+ service: this.serviceName,
654
+ operation: endpoint.operation?.operationId || endpoint.path,
655
+ input: {}
656
+ });
657
+ });
658
+ }
659
+
660
+ return {
661
+ version: '1.0.0',
662
+ steps: steps.length > 0 ? steps : [
663
+ {
664
+ id: 'default-step',
665
+ type: 'task',
666
+ service: this.serviceName,
667
+ operation: 'health',
668
+ input: {}
669
+ }
670
+ ]
671
+ };
672
+ }
673
+
674
+ extractEndpoints(openApiSpec) {
675
+ const endpoints = [];
676
+ if (!openApiSpec?.paths) return endpoints;
677
+
678
+ for (const [path, pathItem] of Object.entries(openApiSpec.paths)) {
679
+ for (const [method, operation] of Object.entries(pathItem)) {
680
+ if (method !== 'parameters' && !method.startsWith('x-')) {
681
+ endpoints.push({ path, method, operation });
682
+ }
683
+ }
684
+ }
685
+
686
+ return endpoints;
687
+ }
688
+
689
+ /**
690
+ * Generate test report
691
+ */
692
+ generateReport(results) {
693
+ const report = [];
694
+
695
+ report.push('=== Test Orchestration Report ===');
696
+ report.push(`Service: ${results.service}`);
697
+ report.push(`Timestamp: ${new Date(results.timestamp).toISOString()}`);
698
+ report.push('');
699
+
700
+ // Level results
701
+ for (const [level, levelResults] of Object.entries(results.levels)) {
702
+ report.push(`Level: ${level.toUpperCase()}`);
703
+ report.push(` Passed: ${levelResults.passed ? 'YES' : 'NO'}`);
704
+ report.push(` Coverage: ${levelResults.coverage}%`);
705
+ report.push(` Tests: ${levelResults.tests.length}`);
706
+
707
+ if (levelResults.tests) {
708
+ levelResults.tests.forEach(test => {
709
+ const status = test.passed ? '✓' : '✗';
710
+ report.push(` ${status} ${test.name}`);
711
+ });
712
+ }
713
+
714
+ if (levelResults.error) {
715
+ report.push(` Error: ${levelResults.error}`);
716
+ }
717
+ report.push('');
718
+ }
719
+
720
+ report.push(`Overall: ${results.passed ? 'PASSED' : 'FAILED'}`);
721
+ report.push(`Recommendation: ${results.recommendation}`);
722
+
723
+ return report.join('\n');
724
+ }
725
+ }
726
+
727
+ module.exports = TestOrchestrator;