@onlineapps/conn-orch-validator 5.0.0 → 6.0.0-rc.1
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/README.md +2 -2
- package/package.json +1 -2
- package/src/CookbookTestRunner.js +34 -9
- package/src/ServiceReadinessValidator.js +65 -3
- package/src/ValidationOrchestrator.js +60 -32
- package/src/WorkflowTestRunner.js +7 -1
- package/src/helpers/README.md +10 -10
- package/src/helpers/createServiceReadinessTests.js +18 -9
- package/src/index.js +7 -6
- package/src/validators/ServiceStructureValidator.js +18 -17
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ legitimate state until every repo adopts the declaration, and never silent.
|
|
|
94
94
|
|
|
95
95
|
```
|
|
96
96
|
services/my-service/
|
|
97
|
-
├──
|
|
97
|
+
├── config/service/ ← Static (gitignored: NO)
|
|
98
98
|
│ ├── config.json
|
|
99
99
|
│ └── operations.json
|
|
100
100
|
├── conn-runtime/ ← Runtime (gitignored: YES)
|
|
@@ -140,7 +140,7 @@ The validator evaluates each service against cumulative implementation standards
|
|
|
140
140
|
|
|
141
141
|
| Level | Name | Checks | Since |
|
|
142
142
|
|-------|------|--------|-------|
|
|
143
|
-
| **v1.0** | Base Service Standard | `
|
|
143
|
+
| **v1.0** | Base Service Standard | `config/service/`, `src/app.js`, `index.js`, valid `config.json` + `operations.json`, `@onlineapps/service-wrapper` dep | 2025-06 |
|
|
144
144
|
| **v1.1** | Multitenancy Standard | `wrapper.tenantContext` configured in `config.json` | 2026-03 |
|
|
145
145
|
| **v1.2** | Business Error Handling | `business_error_contract` (some file under `src/**` imports a `BusinessError` family from `@onlineapps/service-wrapper` or declares the brand `onlineapps.businessError`) + `no_retired_error_hierarchy` (no error class imported from `@onlineapps/service-common` anywhere under `src/`) | 2026-03 |
|
|
146
146
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-validator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0-rc.1",
|
|
4
4
|
"description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "jest",
|
|
11
11
|
"test:unit": "jest tests/unit",
|
|
12
|
-
"test:component": "jest tests/component",
|
|
13
12
|
"test:integration": "jest tests/integration",
|
|
14
13
|
"test:coverage": "jest --coverage"
|
|
15
14
|
},
|
|
@@ -10,6 +10,10 @@ const { checkCookbookFormatVersion, normalizeCookbookSteps } = require('./utils/
|
|
|
10
10
|
const { getTestNamespace } = require('./utils/testNamespace');
|
|
11
11
|
const { describeStepFailure, describeStepIdentity } = require('./utils/stepFailure');
|
|
12
12
|
|
|
13
|
+
// The logger methods this runner requires — the order is the order the error
|
|
14
|
+
// message lists them in.
|
|
15
|
+
const LOGGER_METHODS = ['info', 'warn', 'error', 'debug'];
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* The step's stopwatch, spelled out. A single total hid the fact that
|
|
15
19
|
* biz-hello's `count_stub` spent 2264 ms loading sequelize and ~0 ms doing its
|
|
@@ -37,7 +41,7 @@ function describeStepTiming(result) {
|
|
|
37
41
|
* httpClient: null, secrets: null, state: null, stream: null,
|
|
38
42
|
* abortSignal: null }
|
|
39
43
|
* matching the shape ServiceWrapper.ContextBuilder produces in production
|
|
40
|
-
* (see api/docs/
|
|
44
|
+
* (see api/docs/biz/10-invocation/handler-dispatch.md). Handlers
|
|
41
45
|
* that need DB access import sequelize directly from their own
|
|
42
46
|
* src/config/database.js module (see biz/60-templates/onboarding-checklist.md §9a);
|
|
43
47
|
* connector slots stay null and the handler surfaces its own failure if
|
|
@@ -50,8 +54,29 @@ class CookbookTestRunner {
|
|
|
50
54
|
if (!options.serviceName) {
|
|
51
55
|
throw new Error('[CookbookTestRunner] serviceName is required');
|
|
52
56
|
}
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
// The logger is INJECTED and validated in full, HERE. Until 2026-09-03 only
|
|
58
|
+
// `warn()` was checked — the one method this runner never calls, while it
|
|
59
|
+
// calls info() and error() throughout a run. A logger without them therefore
|
|
60
|
+
// constructed fine and died on the first log line: delayed validation, which
|
|
61
|
+
// architecture-principles.md §4 forbids. Owner confirmation:
|
|
62
|
+
// docs/governance/confirmations/connector-logger-contract.md 001.
|
|
63
|
+
if (!options.logger) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'[CookbookTestRunner] Logger is required - Expected: a logger with info/warn/error/debug, '
|
|
66
|
+
+ 'so the runner writes where the service writes. '
|
|
67
|
+
+ 'Fix: pass options.logger (e.g. the logger your service already built).'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const missingLoggerMethods = LOGGER_METHODS.filter(
|
|
72
|
+
(method) => typeof options.logger[method] !== 'function'
|
|
73
|
+
);
|
|
74
|
+
if (missingLoggerMethods.length > 0) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'[CookbookTestRunner] Logger is incomplete - Expected: info, warn, error, debug as functions; '
|
|
77
|
+
+ `missing: ${missingLoggerMethods.join(', ')}. `
|
|
78
|
+
+ 'Fix: pass a logger implementing all four.'
|
|
79
|
+
);
|
|
55
80
|
}
|
|
56
81
|
this.serviceName = options.serviceName;
|
|
57
82
|
this.servicePath = options.servicePath;
|
|
@@ -310,7 +335,7 @@ class CookbookTestRunner {
|
|
|
310
335
|
/**
|
|
311
336
|
* Handler dispatch: require handler module, build minimal real ctx, call
|
|
312
337
|
* handler(input, ctx). Mirrors the production ContextBuilder shape
|
|
313
|
-
* (see api/docs/
|
|
338
|
+
* (see api/docs/biz/10-invocation/handler-dispatch.md) with
|
|
314
339
|
* all connector slots set to null — handlers that need DB access use
|
|
315
340
|
* direct sequelize import per biz/60-templates/onboarding-checklist.md §9a.
|
|
316
341
|
*/
|
|
@@ -621,12 +646,12 @@ class CookbookTestRunner {
|
|
|
621
646
|
*/
|
|
622
647
|
async resolveOperation(serviceName, operationName) {
|
|
623
648
|
const serviceRoot = this._resolveServiceRoot(serviceName);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const operationsPath =
|
|
649
|
+
// config/service/ is the ONLY configuration path (owner decision
|
|
650
|
+
// 2026-09-03) — the legacy conn-config/ layout exists in no repository.
|
|
651
|
+
const operationsPath = path.join(serviceRoot, 'config', 'service', 'operations.json');
|
|
627
652
|
|
|
628
|
-
if (!operationsPath) {
|
|
629
|
-
throw new Error(`Operations file not found for service: ${serviceName} (
|
|
653
|
+
if (!fs.existsSync(operationsPath)) {
|
|
654
|
+
throw new Error(`Operations file not found for service: ${serviceName} (expected config/service/operations.json)`);
|
|
630
655
|
}
|
|
631
656
|
|
|
632
657
|
const operations = JSON.parse(fs.readFileSync(operationsPath, 'utf8'));
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const CookbookTestUtils = require('./CookbookTestUtils');
|
|
4
4
|
|
|
5
|
+
// The logger methods this validator requires — the order is the order the
|
|
6
|
+
// error message lists them in.
|
|
7
|
+
const LOGGER_METHODS = ['info', 'warn', 'error', 'debug'];
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* ServiceReadinessValidator - Orchestrates complete service validation.
|
|
7
11
|
*
|
|
@@ -32,15 +36,37 @@ const CookbookTestUtils = require('./CookbookTestUtils');
|
|
|
32
36
|
* `tests/bootstrap/` suites of the biz repos — which pass name, version,
|
|
33
37
|
* operations, testCookbook and registry, and never passed a url.
|
|
34
38
|
*
|
|
35
|
-
* @see
|
|
39
|
+
* @see api/docs/biz/30-operations/schema-v3.md
|
|
36
40
|
* @see /api/docs/biz/40-cookbooks/test-runner-flow.md (input probe contract)
|
|
37
41
|
* @see /api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md
|
|
38
42
|
*/
|
|
39
43
|
class ServiceReadinessValidator {
|
|
40
44
|
constructor(options = {}) {
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
// The logger is INJECTED and validated in full, HERE — and, since
|
|
46
|
+
// 2026-09-03, actually used: validateReadiness() writes the verdict through
|
|
47
|
+
// it. Until then this class demanded a logger and never wrote a line, so a
|
|
48
|
+
// caller that did not print the returned object learned nothing about why a
|
|
49
|
+
// service was refused. Owner confirmation:
|
|
50
|
+
// docs/governance/confirmations/connector-logger-contract.md 001.
|
|
51
|
+
if (!options.logger) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'[ServiceReadinessValidator] Logger is required - Expected: a logger with info/warn/error/debug, '
|
|
54
|
+
+ 'so the readiness verdict leaves the process. '
|
|
55
|
+
+ 'Fix: pass options.logger (e.g. the logger your service already built).'
|
|
56
|
+
);
|
|
43
57
|
}
|
|
58
|
+
|
|
59
|
+
const missingLoggerMethods = LOGGER_METHODS.filter(
|
|
60
|
+
(method) => typeof options.logger[method] !== 'function'
|
|
61
|
+
);
|
|
62
|
+
if (missingLoggerMethods.length > 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'[ServiceReadinessValidator] Logger is incomplete - Expected: info, warn, error, debug as functions; '
|
|
65
|
+
+ `missing: ${missingLoggerMethods.join(', ')}. `
|
|
66
|
+
+ 'Fix: pass a logger implementing all four.'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
this.logger = options.logger;
|
|
45
71
|
|
|
46
72
|
// Readiness checks: core (80 points) + optional (20 points) = 100 points max
|
|
@@ -149,6 +175,42 @@ class ServiceReadinessValidator {
|
|
|
149
175
|
results.ready = requiredPassed && results.score >= 60;
|
|
150
176
|
results.recommendation = this.getRecommendation(results);
|
|
151
177
|
|
|
178
|
+
// ONE line per evaluation, carrying the whole verdict as a structured
|
|
179
|
+
// object: what was checked, what each check contributed, and why the answer
|
|
180
|
+
// is what it is. Per check there is nothing — a line per check says the same
|
|
181
|
+
// thing in more places, and this runs on every biz service boot.
|
|
182
|
+
//
|
|
183
|
+
// `score` is read off the check rather than assumed: a check that never
|
|
184
|
+
// reached award() (operations absent entirely) carries no score field, and
|
|
185
|
+
// the log says 0 instead of `undefined`.
|
|
186
|
+
const checkSummary = {};
|
|
187
|
+
for (const [name, check] of Object.entries(results.checks)) {
|
|
188
|
+
checkSummary[name] = {
|
|
189
|
+
passed: check.passed === true,
|
|
190
|
+
score: typeof check.score === 'number' ? check.score : 0
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.logger.info('[ServiceReadinessValidator] Readiness evaluated', {
|
|
195
|
+
serviceName: results.serviceName,
|
|
196
|
+
score: results.score,
|
|
197
|
+
maxScore: results.maxScore,
|
|
198
|
+
ready: results.ready,
|
|
199
|
+
checks: checkSummary,
|
|
200
|
+
errors: results.errors,
|
|
201
|
+
warnings: results.warnings
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// A refusal is an error-level event wherever logs are filtered by level;
|
|
205
|
+
// the info line above still carries the detail.
|
|
206
|
+
if (results.ready === false) {
|
|
207
|
+
this.logger.error('[ServiceReadinessValidator] Service not ready', {
|
|
208
|
+
serviceName: results.serviceName,
|
|
209
|
+
score: results.score,
|
|
210
|
+
errors: results.errors
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
152
214
|
return results;
|
|
153
215
|
}
|
|
154
216
|
|
|
@@ -10,6 +10,10 @@ const { ServiceStructureValidator } = require('./validators/ServiceStructureVali
|
|
|
10
10
|
const { describeStepFailureWithContext } = require('./utils/stepFailure');
|
|
11
11
|
const CookbookTestRunner = require('./CookbookTestRunner');
|
|
12
12
|
|
|
13
|
+
// The logger methods this orchestrator requires — the order is the order the
|
|
14
|
+
// error message lists them in.
|
|
15
|
+
const LOGGER_METHODS = ['info', 'warn', 'error', 'debug'];
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* ValidationOrchestrator
|
|
15
19
|
*
|
|
@@ -36,15 +40,39 @@ class ValidationOrchestrator {
|
|
|
36
40
|
this.serviceRoot = options.serviceRoot;
|
|
37
41
|
this.serviceName = options.serviceName;
|
|
38
42
|
this.serviceVersion = options.serviceVersion;
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
// The logger is INJECTED and validated in full, HERE. Until 2026-09-03 only
|
|
44
|
+
// `warn()` was checked, while this class narrates every step of a run and
|
|
45
|
+
// hands the same object to CookbookTestRunner below, which demands all four.
|
|
46
|
+
// An incomplete logger therefore reached the runner one line later and threw
|
|
47
|
+
// in its name instead of this one. Owner confirmation:
|
|
48
|
+
// docs/governance/confirmations/connector-logger-contract.md 001.
|
|
49
|
+
if (!options.logger) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'[ValidationOrchestrator] Logger is required - Expected: a logger with info/warn/error/debug, '
|
|
52
|
+
+ 'so validation narrates where the service writes. '
|
|
53
|
+
+ 'Fix: pass options.logger (e.g. the logger your service already built).'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const missingLoggerMethods = LOGGER_METHODS.filter(
|
|
58
|
+
(method) => typeof options.logger[method] !== 'function'
|
|
59
|
+
);
|
|
60
|
+
if (missingLoggerMethods.length > 0) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'[ValidationOrchestrator] Logger is incomplete - Expected: info, warn, error, debug as functions; '
|
|
63
|
+
+ `missing: ${missingLoggerMethods.join(', ')}. `
|
|
64
|
+
+ 'Fix: pass a logger implementing all four.'
|
|
65
|
+
);
|
|
41
66
|
}
|
|
67
|
+
|
|
42
68
|
this.logger = options.logger;
|
|
43
69
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
70
|
+
// config/service/ is the ONLY configuration path (owner decision
|
|
71
|
+
// 2026-09-03). The legacy conn-config/ branch is gone: zero such
|
|
72
|
+
// directories existed across api_biz/* and infra/* when it was removed, so
|
|
73
|
+
// it could only ever resolve to a path that does not exist and report the
|
|
74
|
+
// failure one layer away from the cause.
|
|
75
|
+
this.configPath = path.join(this.serviceRoot, 'config', 'service');
|
|
48
76
|
|
|
49
77
|
this.runtimePath = path.join(this.serviceRoot, 'conn-runtime');
|
|
50
78
|
this.proofPath = path.join(this.runtimePath, 'validation-proof.json');
|
|
@@ -64,7 +92,7 @@ class ValidationOrchestrator {
|
|
|
64
92
|
* Checks if proof exists and is valid, or runs full validation
|
|
65
93
|
*/
|
|
66
94
|
async validate() {
|
|
67
|
-
|
|
95
|
+
this.logger.info('[ValidationOrchestrator] Starting validation...');
|
|
68
96
|
|
|
69
97
|
// Check if proof exists and is valid
|
|
70
98
|
// No proof cache. It skipped steps 1-3 and 5-6 — measured at 6 ms in total
|
|
@@ -79,9 +107,9 @@ class ValidationOrchestrator {
|
|
|
79
107
|
// Dockerfile either. Removing it gives up no capability.
|
|
80
108
|
//
|
|
81
109
|
// Its original justification — an HTTP probe with retries in step 5 — was
|
|
82
|
-
// removed in 3.3.1 under ADR 0005.
|
|
110
|
+
// removed in 3.3.1 under ADR 0005.
|
|
83
111
|
// Run full validation
|
|
84
|
-
|
|
112
|
+
this.logger.info('[ValidationOrchestrator] No valid proof, running validation...');
|
|
85
113
|
return await this.runFullValidation();
|
|
86
114
|
}
|
|
87
115
|
|
|
@@ -223,7 +251,7 @@ class ValidationOrchestrator {
|
|
|
223
251
|
|
|
224
252
|
try {
|
|
225
253
|
// Step 1: Service Structure
|
|
226
|
-
|
|
254
|
+
this.logger.info('[ValidationOrchestrator] Step 1/6: Service Structure');
|
|
227
255
|
results.steps.structure = await this.validateStructure();
|
|
228
256
|
if (!results.steps.structure.valid) {
|
|
229
257
|
results.success = false;
|
|
@@ -233,7 +261,7 @@ class ValidationOrchestrator {
|
|
|
233
261
|
}
|
|
234
262
|
|
|
235
263
|
// Step 2: Config Files
|
|
236
|
-
|
|
264
|
+
this.logger.info('[ValidationOrchestrator] Step 2/6: Config Files');
|
|
237
265
|
results.steps.config = await this.validateConfig();
|
|
238
266
|
if (!results.steps.config.valid) {
|
|
239
267
|
results.success = false;
|
|
@@ -247,7 +275,7 @@ class ValidationOrchestrator {
|
|
|
247
275
|
// start without must fail here, where its name and the reason it exists
|
|
248
276
|
// are both at hand. SECRETS_MASTER_KEY used to fail after MQ
|
|
249
277
|
// registration, several layers away from the declaration (defect D2).
|
|
250
|
-
|
|
278
|
+
this.logger.info('[ValidationOrchestrator] Step 3/6: Environment Contract');
|
|
251
279
|
results.steps.env = this.validateEnvContract();
|
|
252
280
|
if (!results.steps.env.valid) {
|
|
253
281
|
results.success = false;
|
|
@@ -258,7 +286,7 @@ class ValidationOrchestrator {
|
|
|
258
286
|
}
|
|
259
287
|
|
|
260
288
|
// Step 4: Operations Compliance
|
|
261
|
-
|
|
289
|
+
this.logger.info('[ValidationOrchestrator] Step 4/6: Operations Compliance');
|
|
262
290
|
results.steps.operations = await this.validateOperations();
|
|
263
291
|
results.warnings.push(...(results.steps.operations.warnings || []));
|
|
264
292
|
if (!results.steps.operations.valid) {
|
|
@@ -267,7 +295,7 @@ class ValidationOrchestrator {
|
|
|
267
295
|
}
|
|
268
296
|
|
|
269
297
|
// Step 5: Cookbook Tests
|
|
270
|
-
|
|
298
|
+
this.logger.info('[ValidationOrchestrator] Step 5/6: Cookbook Tests');
|
|
271
299
|
results.steps.cookbooks = await this.runCookbookTests();
|
|
272
300
|
results.totalTests += results.steps.cookbooks.total || 0;
|
|
273
301
|
results.passedTests += results.steps.cookbooks.passed || 0;
|
|
@@ -278,7 +306,7 @@ class ValidationOrchestrator {
|
|
|
278
306
|
}
|
|
279
307
|
|
|
280
308
|
// Step 6: Connector Integration
|
|
281
|
-
|
|
309
|
+
this.logger.info('[ValidationOrchestrator] Step 6/6: Connector Integration');
|
|
282
310
|
results.steps.connectors = this.validateConnectors();
|
|
283
311
|
if (!results.steps.connectors.valid) {
|
|
284
312
|
// Severity is unchanged: the connector contract stays non-critical and
|
|
@@ -298,7 +326,7 @@ class ValidationOrchestrator {
|
|
|
298
326
|
return await this.finalizeResults(results, startTime);
|
|
299
327
|
|
|
300
328
|
} catch (error) {
|
|
301
|
-
|
|
329
|
+
this.logger.error(`[ValidationOrchestrator] Validation failed: ${error.message}`);
|
|
302
330
|
results.success = false;
|
|
303
331
|
results.errors.push(`Validation error: ${error.message}`);
|
|
304
332
|
return this.finalizeResults(results, startTime);
|
|
@@ -312,7 +340,7 @@ class ValidationOrchestrator {
|
|
|
312
340
|
try {
|
|
313
341
|
const result = this.structureValidator.validate();
|
|
314
342
|
|
|
315
|
-
|
|
343
|
+
this.logger.info(`[ValidationOrchestrator] ✓ Service structure: ${result.valid ? 'PASS' : 'FAIL'}`);
|
|
316
344
|
return result;
|
|
317
345
|
} catch (error) {
|
|
318
346
|
return {
|
|
@@ -350,7 +378,7 @@ class ValidationOrchestrator {
|
|
|
350
378
|
}
|
|
351
379
|
}
|
|
352
380
|
|
|
353
|
-
|
|
381
|
+
this.logger.info(`[ValidationOrchestrator] ✓ Config files: ${errors.length === 0 ? 'PASS' : 'FAIL'}`);
|
|
354
382
|
return {
|
|
355
383
|
valid: errors.length === 0,
|
|
356
384
|
errors: errors
|
|
@@ -413,7 +441,7 @@ class ValidationOrchestrator {
|
|
|
413
441
|
}
|
|
414
442
|
}
|
|
415
443
|
|
|
416
|
-
|
|
444
|
+
this.logger.info(`[ValidationOrchestrator] ✓ Operations compliance: ${errors.length === 0 ? 'PASS' : 'FAIL'}`
|
|
417
445
|
+ `${warnings.length > 0 ? ` (${warnings.length} warning(s))` : ''}`);
|
|
418
446
|
return {
|
|
419
447
|
valid: errors.length === 0,
|
|
@@ -440,7 +468,7 @@ class ValidationOrchestrator {
|
|
|
440
468
|
const cookbooksPath = path.join(this.serviceRoot, 'tests', 'cookbooks');
|
|
441
469
|
|
|
442
470
|
if (!fs.existsSync(cookbooksPath)) {
|
|
443
|
-
|
|
471
|
+
this.logger.warn('[ValidationOrchestrator] No cookbook tests found (tests/cookbooks/ missing)');
|
|
444
472
|
return {
|
|
445
473
|
success: true,
|
|
446
474
|
total: 0,
|
|
@@ -452,7 +480,7 @@ class ValidationOrchestrator {
|
|
|
452
480
|
|
|
453
481
|
const result = await this.cookbookRunner.runCookbooks(cookbooksPath);
|
|
454
482
|
|
|
455
|
-
|
|
483
|
+
this.logger.info(`[ValidationOrchestrator] ✓ Cookbook tests: ${result.passed}/${result.total} passed`);
|
|
456
484
|
|
|
457
485
|
// `N cookbook test(s) failed` was the whole error list until 2026-08-29:
|
|
458
486
|
// the count without a single name, so `Validation failed: 1 cookbook
|
|
@@ -462,7 +490,7 @@ class ValidationOrchestrator {
|
|
|
462
490
|
const failedSteps = (result.steps || []).filter((step) => step.passed === false);
|
|
463
491
|
const details = failedSteps.map(describeStepFailureWithContext);
|
|
464
492
|
for (const detail of details) {
|
|
465
|
-
|
|
493
|
+
this.logger.error(`[ValidationOrchestrator] ✗ ${detail}`);
|
|
466
494
|
}
|
|
467
495
|
|
|
468
496
|
return {
|
|
@@ -517,7 +545,7 @@ class ValidationOrchestrator {
|
|
|
517
545
|
// connector step; what is new here is a declaration that exists and is
|
|
518
546
|
// wrong, and that must stop the boot rather than be worked around.
|
|
519
547
|
if (error.code === 'ENOENT') {
|
|
520
|
-
|
|
548
|
+
this.logger.info('[ValidationOrchestrator] ⊘ Environment contract: SKIPPED — '
|
|
521
549
|
+ `no ${path.relative(this.serviceRoot, contractFile)} in this service`);
|
|
522
550
|
return { valid: true, skipped: true, errors: [] };
|
|
523
551
|
}
|
|
@@ -525,14 +553,14 @@ class ValidationOrchestrator {
|
|
|
525
553
|
}
|
|
526
554
|
|
|
527
555
|
if (declaration === null) {
|
|
528
|
-
|
|
556
|
+
this.logger.info('[ValidationOrchestrator] ⊘ Environment contract: SKIPPED — '
|
|
529
557
|
+ 'config/service/integration-contract.json declares no "env" block '
|
|
530
558
|
+ '(F16 adoption pending; nothing is being checked here)');
|
|
531
559
|
return { valid: true, skipped: true, errors: [] };
|
|
532
560
|
}
|
|
533
561
|
|
|
534
562
|
const result = verifyEnvPresence({ declaration, env: process.env });
|
|
535
|
-
|
|
563
|
+
this.logger.info(`[ValidationOrchestrator] ${result.valid ? '✓' : '✗'} Environment contract: `
|
|
536
564
|
+ `${result.valid ? 'PASS' : 'FAIL'} (${result.checked.length} required name(s) checked: `
|
|
537
565
|
+ `${result.checked.join(', ') || 'none'})`);
|
|
538
566
|
return { valid: result.valid, errors: result.errors };
|
|
@@ -561,7 +589,7 @@ class ValidationOrchestrator {
|
|
|
561
589
|
}
|
|
562
590
|
|
|
563
591
|
const result = verifyConnectorContract({ config, requiredConnectors, env: process.env });
|
|
564
|
-
|
|
592
|
+
this.logger.info(`[ValidationOrchestrator] ${result.valid ? '✓' : '✗'} Connector contract: `
|
|
565
593
|
+ `${result.valid ? 'PASS' : 'FAIL'} (${result.checked.length} checked: ${result.checked.join(', ') || 'none'})`);
|
|
566
594
|
return { valid: result.valid, errors: result.errors };
|
|
567
595
|
}
|
|
@@ -599,20 +627,20 @@ class ValidationOrchestrator {
|
|
|
599
627
|
results.proof = encodedProof;
|
|
600
628
|
results.fingerprint = fingerprint;
|
|
601
629
|
|
|
602
|
-
|
|
603
|
-
|
|
630
|
+
this.logger.info(`[ValidationOrchestrator] ✅ Validation PASSED (${duration}ms)`);
|
|
631
|
+
this.logger.info(`[ValidationOrchestrator] Proof saved to: ${this.proofPath}`);
|
|
604
632
|
} catch (error) {
|
|
605
|
-
|
|
633
|
+
this.logger.error(`[ValidationOrchestrator] Failed to generate proof: ${error.message}`);
|
|
606
634
|
results.success = false;
|
|
607
635
|
results.errors.push(`Proof generation failed: ${error.message}`);
|
|
608
636
|
}
|
|
609
637
|
} else {
|
|
610
|
-
|
|
638
|
+
this.logger.error(`[ValidationOrchestrator] ❌ Validation FAILED (${duration}ms)`);
|
|
611
639
|
// The trailing `(', ')}` was a leftover from a half-finished edit to a
|
|
612
640
|
// template literal: it printed on every failed validation as if it were
|
|
613
641
|
// part of the data. Found while reading this line for the biz-hello
|
|
614
642
|
// diagnosis (2026-08-29).
|
|
615
|
-
|
|
643
|
+
this.logger.error(`[ValidationOrchestrator] Errors: ${JSON.stringify(results.errors, null, 2)}`);
|
|
616
644
|
}
|
|
617
645
|
|
|
618
646
|
return results;
|
|
@@ -635,7 +663,7 @@ class ValidationOrchestrator {
|
|
|
635
663
|
// Save proof
|
|
636
664
|
fs.writeFileSync(this.proofPath, JSON.stringify(proof, null, 2));
|
|
637
665
|
|
|
638
|
-
|
|
666
|
+
this.logger.info(`[ValidationOrchestrator] Proof saved: ${this.proofPath}`);
|
|
639
667
|
} catch (error) {
|
|
640
668
|
throw new Error(`Failed to save proof: ${error.message}`);
|
|
641
669
|
}
|
|
@@ -150,7 +150,13 @@ class WorkflowTestRunner extends EventEmitter {
|
|
|
150
150
|
const resolvedInput = this.resolveInput(input, workflow.context);
|
|
151
151
|
|
|
152
152
|
if (this.debug) {
|
|
153
|
-
|
|
153
|
+
// WHICH operation ran, never WHAT it carried: the resolved input is the
|
|
154
|
+
// workflow's payload, and printing it to stdout put whatever the cookbook
|
|
155
|
+
// held — identifiers, amounts, anything — into the terminal and every log
|
|
156
|
+
// collector behind it. This class takes no logger (constructor above), so
|
|
157
|
+
// the trace stays on console; giving it one is an API change and the
|
|
158
|
+
// owner's call, not a side effect of this fix.
|
|
159
|
+
console.log(`Executing task: ${service}.${operation}`);
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
// Get service from registry
|
package/src/helpers/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Instead of **copying test code** between services, we provide **reusable test he
|
|
|
20
20
|
**File:** `createServiceReadinessTests.js`
|
|
21
21
|
|
|
22
22
|
**What it does:**
|
|
23
|
-
- Validates service structure (
|
|
23
|
+
- Validates service structure (config/service/, src/handlers/, package.json)
|
|
24
24
|
- Validates configuration files (config.json, operations.json)
|
|
25
25
|
- Tests all operations endpoints
|
|
26
26
|
- Validates health endpoint
|
|
@@ -70,7 +70,7 @@ The helper uses `ServiceStructureValidator` to validate service structure BEFORE
|
|
|
70
70
|
|
|
71
71
|
✅ ALL CHECKS PASSED
|
|
72
72
|
|
|
73
|
-
✓ Found Configuration directory:
|
|
73
|
+
✓ Found Configuration directory: config/service
|
|
74
74
|
✓ Found Source code directory: src
|
|
75
75
|
✓ Found Tests directory: tests
|
|
76
76
|
✓ Found valid config.json
|
|
@@ -86,14 +86,14 @@ If validation fails, clear error messages are shown:
|
|
|
86
86
|
```
|
|
87
87
|
❌ ERRORS (2):
|
|
88
88
|
|
|
89
|
-
✗ Required directory missing:
|
|
89
|
+
✗ Required directory missing: config/service/
|
|
90
90
|
Type: MISSING_DIRECTORY
|
|
91
|
-
File:
|
|
92
|
-
Fix: Create directory: mkdir -p
|
|
91
|
+
File: config/service/
|
|
92
|
+
Fix: Create directory: mkdir -p config/service
|
|
93
93
|
|
|
94
|
-
✗ Service configuration missing:
|
|
94
|
+
✗ Service configuration missing: config/service/config.json
|
|
95
95
|
Type: MISSING_CONFIG
|
|
96
|
-
File:
|
|
96
|
+
File: config/service/config.json
|
|
97
97
|
Fix: Create config.json with service metadata. See: /docs/biz/60-templates/service-template.md
|
|
98
98
|
|
|
99
99
|
⚠️ WARNINGS (1):
|
|
@@ -111,8 +111,8 @@ The helper automatically detects and loads:
|
|
|
111
111
|
const serviceRoot = path.resolve(__dirname, '../..');
|
|
112
112
|
|
|
113
113
|
// Standard file locations
|
|
114
|
-
const config = require(path.join(serviceRoot, '
|
|
115
|
-
const operations = require(path.join(serviceRoot, '
|
|
114
|
+
const config = require(path.join(serviceRoot, 'config/service/config.json'));
|
|
115
|
+
const operations = require(path.join(serviceRoot, 'config/service/operations.json'));
|
|
116
116
|
const app = require(path.join(serviceRoot, 'src/app.js'));
|
|
117
117
|
|
|
118
118
|
// Extract metadata
|
|
@@ -157,7 +157,7 @@ function createMyTests(testsDir, options = {}) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// 2. Load config
|
|
160
|
-
const config = require(path.join(serviceRoot, '
|
|
160
|
+
const config = require(path.join(serviceRoot, 'config/service/config.json'));
|
|
161
161
|
|
|
162
162
|
// 3. Create test suite
|
|
163
163
|
describe('My Test Suite @integration', () => {
|
|
@@ -40,6 +40,10 @@ const { MIN_COOKBOOK_FORMAT_VERSION } = require('../utils/cookbookFormat');
|
|
|
40
40
|
* @param {Object} [options] - Optional configuration
|
|
41
41
|
* @param {boolean} [options.includeOptionalChecks=true] - Include cookbook & registry checks
|
|
42
42
|
* @param {number} [options.timeout=15000] - Test timeout in ms
|
|
43
|
+
* @param {Object} [options.logger=console] - Logger handed to
|
|
44
|
+
* ServiceReadinessValidator (info/warn/error/debug). Defaults to `console`,
|
|
45
|
+
* which is the report channel of a jest bootstrap suite — see the comment at
|
|
46
|
+
* the call site and FALLBACKS_INVENTORY.md §5.6.
|
|
43
47
|
*
|
|
44
48
|
* @example
|
|
45
49
|
* // In services/my-service/tests/bootstrap/service-readiness.test.js
|
|
@@ -71,14 +75,11 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
71
75
|
);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
};
|
|
80
|
-
const configPath = resolveConfig('config.json');
|
|
81
|
-
const operationsPath = resolveConfig('operations.json');
|
|
78
|
+
// config/service/ is the ONLY configuration path (owner decision 2026-09-03).
|
|
79
|
+
// The legacy conn-config/ branch is gone — it existed in no repository, so it
|
|
80
|
+
// could only turn a missing file into a read error naming a retired layout.
|
|
81
|
+
const configPath = path.join(serviceRoot, 'config', 'service', 'config.json');
|
|
82
|
+
const operationsPath = path.join(serviceRoot, 'config', 'service', 'operations.json');
|
|
82
83
|
|
|
83
84
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
84
85
|
const operations = JSON.parse(fs.readFileSync(operationsPath, 'utf-8'));
|
|
@@ -113,7 +114,15 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
113
114
|
};
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
// `console` is the intended logger here, not a stand-in for one that is
|
|
118
|
+
// unavailable: this helper builds a jest suite in a service's
|
|
119
|
+
// tests/bootstrap, where stdout IS the report the developer reads, and
|
|
120
|
+
// every one of the eight biz services calls it as
|
|
121
|
+
// `createServiceReadinessTests(__dirname)` from its own repository.
|
|
122
|
+
// A caller that has a real logger passes one; nobody is forced to invent
|
|
123
|
+
// one to run their tests.
|
|
124
|
+
// @see api/docs/standards/FALLBACKS_INVENTORY.md §5.6
|
|
125
|
+
const validator = new ServiceReadinessValidator({ logger: options.logger ?? console });
|
|
117
126
|
const result = await validator.validateReadiness({
|
|
118
127
|
name: serviceName,
|
|
119
128
|
version: serviceVersion,
|
package/src/index.js
CHANGED
|
@@ -20,12 +20,13 @@ const CookbookTestUtils = require('./CookbookTestUtils');
|
|
|
20
20
|
const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
|
|
21
21
|
const ValidationProofGenerator = require('./validators/ValidationProofGenerator');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
try
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// Required like every other export: a broken require fails the load, here,
|
|
24
|
+
// with its own stack. The try/catch that used to guard it printed a WARNING and
|
|
25
|
+
// left `CookbookTestRunner: undefined` on the exports — a fallback (§3) and
|
|
26
|
+
// delayed validation (§4) that could not even do what it promised, since
|
|
27
|
+
// ./ValidationOrchestrator below requires the same module unguarded and took
|
|
28
|
+
// the load down anyway. All it added was a misleading line before the real one.
|
|
29
|
+
const CookbookTestRunner = require('./CookbookTestRunner');
|
|
29
30
|
const WorkflowTestRunner = require('./WorkflowTestRunner');
|
|
30
31
|
const BizCiGateContract = require('./utils/bizCiGateContract');
|
|
31
32
|
|
|
@@ -16,25 +16,26 @@ const fs = require('fs');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* Read a configuration file from the ONE place a service may keep it:
|
|
20
|
+
* `config/service/`. The legacy `conn-config/` alternative was removed on
|
|
21
|
+
* 2026-09-03 (owner decision) — no repository carried such a directory any
|
|
22
|
+
* more, so accepting it only taught a shape nobody may build and let every
|
|
23
|
+
* finding offer a fix pointing at a retired layout.
|
|
24
|
+
*
|
|
20
25
|
* @param {string} root - Service root
|
|
21
26
|
* @param {string} filename - e.g. 'config.json'
|
|
22
27
|
* @returns {string|null} File contents or null
|
|
23
28
|
*/
|
|
24
29
|
function readConfigFile(root, filename) {
|
|
25
|
-
|
|
26
|
-
path.join(root, 'config', 'service', filename),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
for (const p of candidates) {
|
|
30
|
-
try { return fs.readFileSync(p, 'utf-8'); } catch { /* try next */ }
|
|
30
|
+
try {
|
|
31
|
+
return fs.readFileSync(path.join(root, 'config', 'service', filename), 'utf-8');
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
31
34
|
}
|
|
32
|
-
return null;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
function hasConfigDir(root) {
|
|
36
|
-
return fs.existsSync(path.join(root, 'config', 'service'))
|
|
37
|
-
fs.existsSync(path.join(root, 'conn-config'));
|
|
38
|
+
return fs.existsSync(path.join(root, 'config', 'service'));
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
|
|
@@ -225,7 +226,7 @@ const STANDARD_LEVELS = [
|
|
|
225
226
|
const has = (p) => fs.existsSync(path.join(root, p));
|
|
226
227
|
const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
|
|
227
228
|
|
|
228
|
-
results.push({ passed: hasConfigDir(root), id: '
|
|
229
|
+
results.push({ passed: hasConfigDir(root), id: 'dir_config_service', message: 'config/service/ directory' });
|
|
229
230
|
results.push({ passed: has('src'), id: 'dir_src', message: 'src/ directory' });
|
|
230
231
|
results.push({ passed: has('tests'), id: 'dir_tests', message: 'tests/ directory' });
|
|
231
232
|
// ADR 0005: src/handlers/ replaces src/app.js. Flat handlers/ or
|
|
@@ -449,8 +450,8 @@ class ServiceStructureValidator {
|
|
|
449
450
|
if (!hasConfigDir(this.serviceRoot)) {
|
|
450
451
|
this.errors.push({
|
|
451
452
|
type: 'MISSING_DIRECTORY',
|
|
452
|
-
path: 'config/service/
|
|
453
|
-
message: 'Required directory missing: config/service/
|
|
453
|
+
path: 'config/service/',
|
|
454
|
+
message: 'Required directory missing: config/service/',
|
|
454
455
|
description: 'Configuration directory',
|
|
455
456
|
fix: 'Create directory: mkdir -p config/service'
|
|
456
457
|
});
|
|
@@ -501,8 +502,8 @@ class ServiceStructureValidator {
|
|
|
501
502
|
if (!configRaw) {
|
|
502
503
|
this.errors.push({
|
|
503
504
|
type: 'MISSING_CONFIG',
|
|
504
|
-
path: 'config/service/config.json
|
|
505
|
-
message: 'Service configuration missing: config/service/config.json
|
|
505
|
+
path: 'config/service/config.json',
|
|
506
|
+
message: 'Service configuration missing: config/service/config.json',
|
|
506
507
|
fix: 'Create config.json with service metadata. See: /docs/biz/60-templates/service-template.md'
|
|
507
508
|
});
|
|
508
509
|
} else {
|
|
@@ -524,8 +525,8 @@ class ServiceStructureValidator {
|
|
|
524
525
|
if (!opsRaw) {
|
|
525
526
|
this.errors.push({
|
|
526
527
|
type: 'MISSING_OPERATIONS',
|
|
527
|
-
path: 'config/service/operations.json
|
|
528
|
-
message: 'Operations specification missing: config/service/operations.json
|
|
528
|
+
path: 'config/service/operations.json',
|
|
529
|
+
message: 'Operations specification missing: config/service/operations.json',
|
|
529
530
|
fix: 'Create operations.json. See: /docs/biz/30-operations/schema-v3.md'
|
|
530
531
|
});
|
|
531
532
|
} else {
|