@onlineapps/conn-orch-validator 3.3.2 → 4.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.
- package/CHANGELOG.md +358 -0
- package/README.md +46 -6
- package/TESTING_STRATEGY.md +1 -1
- package/docs/DESIGN.md +18 -11
- package/package.json +2 -3
- package/src/CookbookTestRunner.js +155 -26
- package/src/CookbookTestUtils.js +12 -4
- package/src/ServiceReadinessValidator.js +35 -18
- package/src/ValidationOrchestrator.js +223 -126
- package/src/cli/biz-ci-gate.js +356 -7
- package/src/helpers/README.md +3 -55
- package/src/helpers/createServiceReadinessTests.js +5 -1
- package/src/index.js +5 -2
- package/src/mocks/MockMQClient.js +100 -15
- package/src/utils/bizCiGateContract.js +110 -21
- package/src/utils/connectorContract.js +96 -0
- package/src/utils/cookbookFormat.js +95 -0
- package/src/utils/deployContract.js +488 -0
- package/src/utils/envContract.js +417 -0
- package/src/utils/installContract.js +142 -0
- package/src/utils/integrationRun.js +297 -0
- package/src/utils/libCompat.js +158 -0
- package/src/utils/preValidation.js +137 -0
- package/src/utils/setupDatabase.js +154 -0
- package/src/utils/stepFailure.js +73 -0
- package/src/utils/testNamespace.js +104 -0
- package/src/validators/ServiceStructureValidator.js +195 -48
- package/src/config.js +0 -32
- package/src/defaults.js +0 -11
- package/src/helpers/createPreValidationTests.js +0 -326
- package/test-mq-flow.js +0 -72
- package/test-orchestrator.js +0 -95
|
@@ -2,35 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { verifyConnectorContract } = require('./utils/connectorContract');
|
|
6
|
+
const { normalizeEnvDeclaration, collectEnvCoverage, verifyEnvPresence } = require('./utils/envContract');
|
|
5
7
|
const { ValidationProofCodec, ValidationProofVerifier } = require('@onlineapps/service-validator-core');
|
|
6
8
|
const FingerprintUtils = require('@onlineapps/service-validator-core').FingerprintUtils;
|
|
7
9
|
const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
|
|
8
|
-
const
|
|
10
|
+
const { describeStepFailureWithContext } = require('./utils/stepFailure');
|
|
9
11
|
const CookbookTestRunner = require('./CookbookTestRunner');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* ValidationOrchestrator
|
|
13
15
|
*
|
|
14
16
|
* Orchestrates complete service validation (Tier 1 Pre-Validation)
|
|
15
|
-
* Runs 6-step validation process and generates validation proof.
|
|
17
|
+
* Runs a 6-step validation process and generates validation proof.
|
|
16
18
|
*
|
|
17
19
|
* Called automatically by ServiceWrapper during initialization.
|
|
20
|
+
*
|
|
21
|
+
* The steps are not the same six it ran before 2026-08-22. The readiness step
|
|
22
|
+
* is gone — its removal, and why nothing was lost with it, is explained above
|
|
23
|
+
* runFullValidation() — and the environment contract (step 3, F16) took a
|
|
24
|
+
* different place: a service now fails on a variable it declared it needs
|
|
25
|
+
* before any handler runs, instead of several layers later inside the wrapper.
|
|
26
|
+
*
|
|
27
|
+
* `options.serviceUrl` is accepted-and-ignored: existing callers
|
|
28
|
+
* (ServiceWrapper, the biz `scripts/run-pre-validation.js`) still pass one.
|
|
29
|
+
* It is not stored and not required. It fed the readiness step's
|
|
30
|
+
* `fetch(url + '/health')`, retired in 3.3.1 under ADR 0005; after that it was
|
|
31
|
+
* a *required* constructor option no line of code read, so a service whose
|
|
32
|
+
* SERVICE_URL was unset failed to boot on account of a value nobody wanted.
|
|
18
33
|
*/
|
|
19
34
|
class ValidationOrchestrator {
|
|
20
35
|
constructor(options = {}) {
|
|
21
36
|
this.serviceRoot = options.serviceRoot;
|
|
22
37
|
this.serviceName = options.serviceName;
|
|
23
38
|
this.serviceVersion = options.serviceVersion;
|
|
24
|
-
this.serviceUrl = options.serviceUrl; // NEW: Service URL for HTTP calls
|
|
25
39
|
if (!options.logger || typeof options.logger.warn !== 'function') {
|
|
26
40
|
throw new Error('[ValidationOrchestrator] Logger is required — Expected object with warn() method');
|
|
27
41
|
}
|
|
28
42
|
this.logger = options.logger;
|
|
29
43
|
|
|
30
|
-
if (!this.serviceUrl) {
|
|
31
|
-
throw new Error('[ValidationOrchestrator] serviceUrl is required');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
44
|
// Paths — prefer new config/service/ layout, fall back to legacy conn-config/
|
|
35
45
|
const newConfigDir = path.join(this.serviceRoot, 'config', 'service');
|
|
36
46
|
const legacyConfigDir = path.join(this.serviceRoot, 'conn-config');
|
|
@@ -41,7 +51,6 @@ class ValidationOrchestrator {
|
|
|
41
51
|
|
|
42
52
|
// Validators
|
|
43
53
|
this.structureValidator = new ServiceStructureValidator(this.serviceRoot);
|
|
44
|
-
this.readinessValidator = new ServiceReadinessValidator({ logger: this.logger });
|
|
45
54
|
this.cookbookRunner = new CookbookTestRunner({
|
|
46
55
|
servicePath: this.serviceRoot,
|
|
47
56
|
serviceName: this.serviceName,
|
|
@@ -58,78 +67,25 @@ class ValidationOrchestrator {
|
|
|
58
67
|
console.log('[ValidationOrchestrator] Starting validation...');
|
|
59
68
|
|
|
60
69
|
// Check if proof exists and is valid
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// No proof cache. It skipped steps 1-3 and 5-6 — measured at 6 ms in total
|
|
71
|
+
// against 1 ms to compute the fingerprint that decided it, so it bought five
|
|
72
|
+
// milliseconds. It cost a window of up to seven days in which validation
|
|
73
|
+
// asserted something no longer true, which is how biz-converter's
|
|
74
|
+
// fresh-database failure stayed hidden for days.
|
|
75
|
+
//
|
|
76
|
+
// And it never worked: FingerprintUtils.generate() serialised with a
|
|
77
|
+
// replacer array, so the fingerprint only ever covered serviceVersion and
|
|
78
|
+
// never invalidated on a change to operations.json, config.json or the
|
|
79
|
+
// Dockerfile either. Removing it gives up no capability.
|
|
80
|
+
//
|
|
81
|
+
// Its original justification — an HTTP probe with retries in step 5 — was
|
|
82
|
+
// removed in 3.3.1 under ADR 0005. Audit: docs/archive/validation-proof-audit-2026-08.md
|
|
72
83
|
// Run full validation
|
|
73
84
|
console.log('[ValidationOrchestrator] No valid proof, running validation...');
|
|
74
85
|
return await this.runFullValidation();
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
/**
|
|
78
|
-
* Load existing validation proof from conn-runtime/
|
|
79
|
-
*/
|
|
80
|
-
async loadExistingProof() {
|
|
81
|
-
try {
|
|
82
|
-
if (!fs.existsSync(this.proofPath)) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const content = fs.readFileSync(this.proofPath, 'utf8');
|
|
87
|
-
const proof = JSON.parse(content);
|
|
88
|
-
|
|
89
|
-
return proof;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
console.warn(`[ValidationOrchestrator] Failed to load proof: ${error.message}`);
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Check if proof is still valid
|
|
98
|
-
* Valid = correct signature + fingerprint + not expired
|
|
99
|
-
*/
|
|
100
|
-
async isProofValid(proof) {
|
|
101
|
-
try {
|
|
102
|
-
if (!proof) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Verify proof integrity + age using the shared core verifier defaults.
|
|
107
|
-
// This MUST match what Tier-2 validator enforces to avoid PROOF_EXPIRED drift.
|
|
108
|
-
const verifier = new ValidationProofVerifier();
|
|
109
|
-
const verificationResult = verifier.verifyProof(proof);
|
|
110
|
-
|
|
111
|
-
if (!verificationResult.valid) {
|
|
112
|
-
console.log(`[ValidationOrchestrator] Proof validation failed: ${verificationResult.reason}`);
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Calculate current fingerprint
|
|
117
|
-
const currentFingerprint = await this.calculateFingerprint();
|
|
118
88
|
|
|
119
|
-
// Check if fingerprint matches (stored in validationData.contractFingerprint)
|
|
120
|
-
if (proof.validationData?.contractFingerprint !== currentFingerprint) {
|
|
121
|
-
console.log('[ValidationOrchestrator] Fingerprint mismatch (service changed)');
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return true;
|
|
126
|
-
} catch (error) {
|
|
127
|
-
if (this.logger && typeof this.logger.error === 'function') {
|
|
128
|
-
this.logger.error(`[ValidationOrchestrator] Proof validation error: ${error.message}`);
|
|
129
|
-
}
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
89
|
|
|
134
90
|
/**
|
|
135
91
|
* Calculate service fingerprint
|
|
@@ -164,6 +120,34 @@ class ValidationOrchestrator {
|
|
|
164
120
|
infra.envTemplate = fs.readFileSync(envTemplateFile, 'utf8');
|
|
165
121
|
}
|
|
166
122
|
|
|
123
|
+
// Cookbooks and seeds are validation INPUTS. Leaving them out let a proof
|
|
124
|
+
// outlive the very thing it attests to: editing a cookbook, or breaking
|
|
125
|
+
// the fixtures one depends on, kept a cached proof valid for up to seven
|
|
126
|
+
// days while the service booted green. That masked biz-converter's
|
|
127
|
+
// fresh-database boot failure for days. Step 4 now also runs every boot
|
|
128
|
+
// regardless (see validate()); this covers the cached structural steps.
|
|
129
|
+
// A missing directory means the service has none of these, which is
|
|
130
|
+
// legitimate. Any other read failure is not, and must reach the caller
|
|
131
|
+
// rather than silently reducing what the fingerprint covers.
|
|
132
|
+
const hashDirectory = (dir, extension) => {
|
|
133
|
+
let names;
|
|
134
|
+
try {
|
|
135
|
+
names = fs.readdirSync(dir);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err.code === 'ENOENT') return undefined;
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
const files = names.filter((name) => name.endsWith(extension)).sort();
|
|
141
|
+
if (files.length === 0) return undefined;
|
|
142
|
+
return files.map((name) => `${name}:${fs.readFileSync(path.join(dir, name), 'utf8')}`).join('\n');
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const cookbooks = hashDirectory(path.join(this.serviceRoot, 'tests', 'cookbooks'), '.json');
|
|
146
|
+
if (cookbooks !== undefined) infra.cookbooks = cookbooks;
|
|
147
|
+
|
|
148
|
+
const seeds = hashDirectory(path.join(this.serviceRoot, 'scripts', 'seed'), '.sql');
|
|
149
|
+
if (seeds !== undefined) infra.seeds = seeds;
|
|
150
|
+
|
|
167
151
|
// Extract @onlineapps/* dependencies
|
|
168
152
|
const deps = {};
|
|
169
153
|
if (pkg.dependencies) {
|
|
@@ -190,7 +174,40 @@ class ValidationOrchestrator {
|
|
|
190
174
|
}
|
|
191
175
|
|
|
192
176
|
/**
|
|
193
|
-
* Run full 6-step validation process
|
|
177
|
+
* Run the full 6-step validation process.
|
|
178
|
+
*
|
|
179
|
+
* There were six steps until 2026-08-22. Step 5, "Service Readiness",
|
|
180
|
+
* delegated to `ServiceReadinessValidator.validateReadiness({name, url,
|
|
181
|
+
* operations})` and reported a sixth-of-the-run verdict it never
|
|
182
|
+
* independently reached:
|
|
183
|
+
*
|
|
184
|
+
* - Its per-operation rules were the same set as step 3's, rule for rule
|
|
185
|
+
* (handler presence and shape, bundle_scope, input, output, the retired
|
|
186
|
+
* v2 fields) — the two loops were textual copies of one another.
|
|
187
|
+
* - Its only two extra guards, "operations must be an object" and "no
|
|
188
|
+
* operations defined", are step 2's `operations.json has no operations
|
|
189
|
+
* defined`.
|
|
190
|
+
* - Run against a valid baseline plus 13 mutations covering every rule,
|
|
191
|
+
* its verdict equalled `step2 && step3` in all 14 cases. Against the
|
|
192
|
+
* eight live biz services it emitted one check, scored 80 out of a
|
|
193
|
+
* maxScore of 100 that this call path could never reach (the 15-point
|
|
194
|
+
* cookbook and 5-point registry checks need arguments the orchestrator
|
|
195
|
+
* never passed), and compared that 80 against a `score >= 60` threshold
|
|
196
|
+
* the single remaining weight had made decorative. It measured 0 ms.
|
|
197
|
+
*
|
|
198
|
+
* So it could not fail a service that steps 2 and 3 passed. A step that
|
|
199
|
+
* announces PASS without evaluating anything of its own is the defect the
|
|
200
|
+
* connector step was fixed for in F12 (`dcddb862`); this is the same defect
|
|
201
|
+
* one step earlier, and the honest repair is removal, not a smaller lie.
|
|
202
|
+
*
|
|
203
|
+
* Its one non-duplicate signal — a warning for an operation with no
|
|
204
|
+
* `description` — never reached anyone: the step returned `{valid, errors}`
|
|
205
|
+
* and dropped `warnings` on the floor. That signal now lives in step 3 and
|
|
206
|
+
* is surfaced in `results.warnings`, so removal costs nothing.
|
|
207
|
+
*
|
|
208
|
+
* `ServiceReadinessValidator` itself stays: its cookbook and registry checks
|
|
209
|
+
* are used by `helpers/createServiceReadinessTests`, and through it by the
|
|
210
|
+
* `tests/bootstrap/` suites of the biz repos.
|
|
194
211
|
*/
|
|
195
212
|
async runFullValidation() {
|
|
196
213
|
const startTime = Date.now();
|
|
@@ -223,16 +240,34 @@ class ValidationOrchestrator {
|
|
|
223
240
|
results.errors.push(...results.steps.config.errors);
|
|
224
241
|
}
|
|
225
242
|
|
|
226
|
-
// Step 3:
|
|
227
|
-
|
|
243
|
+
// Step 3: Environment Contract
|
|
244
|
+
//
|
|
245
|
+
// Before the cookbook tests run a single handler and long before the
|
|
246
|
+
// wrapper opens a connector: a variable the service declared it cannot
|
|
247
|
+
// start without must fail here, where its name and the reason it exists
|
|
248
|
+
// are both at hand. SECRETS_MASTER_KEY used to fail after MQ
|
|
249
|
+
// registration, several layers away from the declaration (defect D2).
|
|
250
|
+
console.log('[ValidationOrchestrator] Step 3/6: Environment Contract');
|
|
251
|
+
results.steps.env = this.validateEnvContract();
|
|
252
|
+
if (!results.steps.env.valid) {
|
|
253
|
+
results.success = false;
|
|
254
|
+
results.errors.push(...results.steps.env.errors);
|
|
255
|
+
// Fail fast — running handlers without the environment they declared
|
|
256
|
+
// produces a second, less honest error somewhere further in.
|
|
257
|
+
return this.finalizeResults(results, startTime);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Step 4: Operations Compliance
|
|
261
|
+
console.log('[ValidationOrchestrator] Step 4/6: Operations Compliance');
|
|
228
262
|
results.steps.operations = await this.validateOperations();
|
|
263
|
+
results.warnings.push(...(results.steps.operations.warnings || []));
|
|
229
264
|
if (!results.steps.operations.valid) {
|
|
230
265
|
results.success = false;
|
|
231
266
|
results.errors.push(...results.steps.operations.errors);
|
|
232
267
|
}
|
|
233
268
|
|
|
234
|
-
// Step
|
|
235
|
-
console.log('[ValidationOrchestrator] Step
|
|
269
|
+
// Step 5: Cookbook Tests
|
|
270
|
+
console.log('[ValidationOrchestrator] Step 5/6: Cookbook Tests');
|
|
236
271
|
results.steps.cookbooks = await this.runCookbookTests();
|
|
237
272
|
results.totalTests += results.steps.cookbooks.total || 0;
|
|
238
273
|
results.passedTests += results.steps.cookbooks.passed || 0;
|
|
@@ -242,20 +277,21 @@ class ValidationOrchestrator {
|
|
|
242
277
|
results.errors.push(...(results.steps.cookbooks.errors || []));
|
|
243
278
|
}
|
|
244
279
|
|
|
245
|
-
// Step 5: Service Readiness
|
|
246
|
-
console.log('[ValidationOrchestrator] Step 5/6: Service Readiness');
|
|
247
|
-
results.steps.readiness = await this.validateReadiness();
|
|
248
|
-
if (!results.steps.readiness.valid) {
|
|
249
|
-
results.success = false;
|
|
250
|
-
results.errors.push(...results.steps.readiness.errors);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
280
|
// Step 6: Connector Integration
|
|
254
281
|
console.log('[ValidationOrchestrator] Step 6/6: Connector Integration');
|
|
255
282
|
results.steps.connectors = this.validateConnectors();
|
|
256
283
|
if (!results.steps.connectors.valid) {
|
|
257
|
-
//
|
|
258
|
-
|
|
284
|
+
// Severity is unchanged: the connector contract stays non-critical and
|
|
285
|
+
// its findings are reported as warnings, not as validation errors.
|
|
286
|
+
//
|
|
287
|
+
// The field they are read from is what changed. F12 (`dcddb862`) gave
|
|
288
|
+
// this step a real check returning {valid, errors}; this line still
|
|
289
|
+
// spread `.warnings`, which that shape does not have. So the only case
|
|
290
|
+
// the step exists to catch — a service that actually fails the contract
|
|
291
|
+
// — threw "results.steps.connectors.warnings is not iterable", aborted
|
|
292
|
+
// the whole run, and reported that TypeError in place of every finding.
|
|
293
|
+
// Reproduced against biz-pdfgen and biz-property on 2026-08-22.
|
|
294
|
+
results.warnings.push(...results.steps.connectors.errors);
|
|
259
295
|
}
|
|
260
296
|
|
|
261
297
|
// Finalize and generate proof if successful
|
|
@@ -331,18 +367,26 @@ class ValidationOrchestrator {
|
|
|
331
367
|
* Step 3: Validate operations compliance (v3 — handler registry dispatch).
|
|
332
368
|
* Required per operation: handler ('handlers/<path>#<export>'), bundle_scope, input, output.
|
|
333
369
|
* Forbidden (v2): endpoint, method, path.
|
|
370
|
+
* Warned about: a missing `description` — inherited from the removed
|
|
371
|
+
* readiness step, which was the only place that noticed it and then threw
|
|
372
|
+
* the notice away.
|
|
334
373
|
*/
|
|
335
374
|
async validateOperations() {
|
|
336
375
|
try {
|
|
337
376
|
const operationsFile = path.join(this.configPath, 'operations.json');
|
|
338
377
|
const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
|
|
339
378
|
const errors = [];
|
|
379
|
+
const warnings = [];
|
|
340
380
|
|
|
341
381
|
const validScopes = ['platform', 'tenant', 'workspace'];
|
|
342
382
|
const handlerPattern = /^handlers\/[a-zA-Z0-9_\/-]+#[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
343
383
|
const forbiddenV2Fields = ['endpoint', 'method', 'path'];
|
|
344
384
|
|
|
345
385
|
for (const [opName, opDef] of Object.entries(operations.operations || {})) {
|
|
386
|
+
if (!opDef.description) {
|
|
387
|
+
warnings.push(`Operation ${opName}: missing description`);
|
|
388
|
+
}
|
|
389
|
+
|
|
346
390
|
if (!opDef.handler) {
|
|
347
391
|
errors.push(`Operation ${opName}: missing handler (v3 — 'handlers/<path>#<export>')`);
|
|
348
392
|
} else if (!handlerPattern.test(opDef.handler)) {
|
|
@@ -369,15 +413,18 @@ class ValidationOrchestrator {
|
|
|
369
413
|
}
|
|
370
414
|
}
|
|
371
415
|
|
|
372
|
-
console.log(`[ValidationOrchestrator] ✓ Operations compliance: ${errors.length === 0 ? 'PASS' : 'FAIL'}`
|
|
416
|
+
console.log(`[ValidationOrchestrator] ✓ Operations compliance: ${errors.length === 0 ? 'PASS' : 'FAIL'}`
|
|
417
|
+
+ `${warnings.length > 0 ? ` (${warnings.length} warning(s))` : ''}`);
|
|
373
418
|
return {
|
|
374
419
|
valid: errors.length === 0,
|
|
375
|
-
errors: errors
|
|
420
|
+
errors: errors,
|
|
421
|
+
warnings: warnings
|
|
376
422
|
};
|
|
377
423
|
} catch (error) {
|
|
378
424
|
return {
|
|
379
425
|
valid: false,
|
|
380
|
-
errors: [`Operations validation failed: ${error.message}`]
|
|
426
|
+
errors: [`Operations validation failed: ${error.message}`],
|
|
427
|
+
warnings: []
|
|
381
428
|
};
|
|
382
429
|
}
|
|
383
430
|
}
|
|
@@ -406,12 +453,24 @@ class ValidationOrchestrator {
|
|
|
406
453
|
const result = await this.cookbookRunner.runCookbooks(cookbooksPath);
|
|
407
454
|
|
|
408
455
|
console.log(`[ValidationOrchestrator] ✓ Cookbook tests: ${result.passed}/${result.total} passed`);
|
|
456
|
+
|
|
457
|
+
// `N cookbook test(s) failed` was the whole error list until 2026-08-29:
|
|
458
|
+
// the count without a single name, so `Validation failed: 1 cookbook
|
|
459
|
+
// test(s) failed` was all the wrapper, the registry and the reader ever
|
|
460
|
+
// got. Every failing step now names itself and its reason
|
|
461
|
+
// (utils/stepFailure.js) — automation-gates.md §5.
|
|
462
|
+
const failedSteps = (result.steps || []).filter((step) => step.passed === false);
|
|
463
|
+
const details = failedSteps.map(describeStepFailureWithContext);
|
|
464
|
+
for (const detail of details) {
|
|
465
|
+
console.error(`[ValidationOrchestrator] ✗ ${detail}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
409
468
|
return {
|
|
410
469
|
success: result.failed === 0,
|
|
411
470
|
total: result.total,
|
|
412
471
|
passed: result.passed,
|
|
413
472
|
failed: result.failed,
|
|
414
|
-
errors: result.failed > 0 ? [`${result.failed} cookbook test(s) failed
|
|
473
|
+
errors: result.failed > 0 ? [`${result.failed} cookbook test(s) failed`, ...details] : []
|
|
415
474
|
};
|
|
416
475
|
} catch (error) {
|
|
417
476
|
return {
|
|
@@ -425,52 +484,86 @@ class ValidationOrchestrator {
|
|
|
425
484
|
}
|
|
426
485
|
|
|
427
486
|
/**
|
|
428
|
-
* Step
|
|
487
|
+
* Step 3: Verify the environment contract — PRESENCE.
|
|
488
|
+
*
|
|
489
|
+
* The service declares the variables it needs in the same file where it
|
|
490
|
+
* declares its connectors and its database; this step checks that every name
|
|
491
|
+
* declared `required` is actually set. CI checks the other direction —
|
|
492
|
+
* COMPLETENESS, that nothing the repo reads is undeclared — because that one
|
|
493
|
+
* needs the repository, not the running environment.
|
|
494
|
+
* Rules and rationale: utils/envContract.js.
|
|
495
|
+
*
|
|
496
|
+
* A contract with no `env` block is skipped OUT LOUD. That state is
|
|
497
|
+
* legitimate until every repo adopts the declaration, but a step that says
|
|
498
|
+
* nothing about what it did not check reads as a guarantee it never gave
|
|
499
|
+
* (automation-gates.md §5).
|
|
500
|
+
*
|
|
501
|
+
* @returns {{valid: boolean, skipped?: boolean, errors: string[]}}
|
|
429
502
|
*/
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
// Load operations.json
|
|
433
|
-
const operationsFile = path.join(this.configPath, 'operations.json');
|
|
434
|
-
const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
|
|
503
|
+
validateEnvContract() {
|
|
504
|
+
const contractFile = path.join(this.serviceRoot, 'config', 'service', 'integration-contract.json');
|
|
435
505
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
506
|
+
let declaration;
|
|
507
|
+
try {
|
|
508
|
+
const rawContract = JSON.parse(fs.readFileSync(contractFile, 'utf8'));
|
|
509
|
+
declaration = normalizeEnvDeclaration(rawContract.env, {
|
|
510
|
+
coverage: collectEnvCoverage({
|
|
511
|
+
serviceRoot: this.serviceRoot,
|
|
512
|
+
requiredConnectors: rawContract.requiredConnectors
|
|
513
|
+
})
|
|
440
514
|
});
|
|
441
|
-
|
|
442
|
-
console.log(`[ValidationOrchestrator] ✓ Service readiness: ${result.ready ? 'PASS' : 'FAIL'}`);
|
|
443
|
-
return {
|
|
444
|
-
valid: result.ready,
|
|
445
|
-
errors: result.ready ? [] : result.errors || ['Service not ready']
|
|
446
|
-
};
|
|
447
515
|
} catch (error) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
516
|
+
// A missing or unreadable contract file is already reported by the
|
|
517
|
+
// connector step; what is new here is a declaration that exists and is
|
|
518
|
+
// wrong, and that must stop the boot rather than be worked around.
|
|
519
|
+
if (error.code === 'ENOENT') {
|
|
520
|
+
console.log('[ValidationOrchestrator] ⊘ Environment contract: SKIPPED — '
|
|
521
|
+
+ `no ${path.relative(this.serviceRoot, contractFile)} in this service`);
|
|
522
|
+
return { valid: true, skipped: true, errors: [] };
|
|
523
|
+
}
|
|
524
|
+
return { valid: false, errors: [error.message] };
|
|
452
525
|
}
|
|
526
|
+
|
|
527
|
+
if (declaration === null) {
|
|
528
|
+
console.log('[ValidationOrchestrator] ⊘ Environment contract: SKIPPED — '
|
|
529
|
+
+ 'config/service/integration-contract.json declares no "env" block '
|
|
530
|
+
+ '(F16 adoption pending; nothing is being checked here)');
|
|
531
|
+
return { valid: true, skipped: true, errors: [] };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const result = verifyEnvPresence({ declaration, env: process.env });
|
|
535
|
+
console.log(`[ValidationOrchestrator] ${result.valid ? '✓' : '✗'} Environment contract: `
|
|
536
|
+
+ `${result.valid ? 'PASS' : 'FAIL'} (${result.checked.length} required name(s) checked: `
|
|
537
|
+
+ `${result.checked.join(', ') || 'none'})`);
|
|
538
|
+
return { valid: result.valid, errors: result.errors };
|
|
453
539
|
}
|
|
454
540
|
|
|
455
541
|
/**
|
|
456
542
|
* Step 6: Validate connector integration
|
|
457
543
|
*/
|
|
544
|
+
/**
|
|
545
|
+
* Verify the connector contract. Tier-1 runs in phase 0.2, before the wrapper
|
|
546
|
+
* opens anything, so nothing live can be probed — what is checked is that the
|
|
547
|
+
* service's two declarations agree and that the environment backs them.
|
|
548
|
+
* Rules and rationale: utils/connectorContract.js.
|
|
549
|
+
*/
|
|
458
550
|
validateConnectors() {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
// which test ServiceWrapper + all connectors
|
|
551
|
+
const configFile = path.join(this.configPath, 'config.json');
|
|
552
|
+
const contractFile = path.join(this.serviceRoot, 'config', 'service', 'integration-contract.json');
|
|
462
553
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
};
|
|
554
|
+
let config = {};
|
|
555
|
+
let requiredConnectors = {};
|
|
556
|
+
try {
|
|
557
|
+
config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
558
|
+
requiredConnectors = JSON.parse(fs.readFileSync(contractFile, 'utf8')).requiredConnectors || {};
|
|
468
559
|
} catch (error) {
|
|
469
|
-
return {
|
|
470
|
-
valid: false,
|
|
471
|
-
warnings: [`Connector validation warning: ${error.message}`]
|
|
472
|
-
};
|
|
560
|
+
return { valid: false, errors: [`[ConnectorContract] Cannot read the connector declarations - ${error.message}`] };
|
|
473
561
|
}
|
|
562
|
+
|
|
563
|
+
const result = verifyConnectorContract({ config, requiredConnectors, env: process.env });
|
|
564
|
+
console.log(`[ValidationOrchestrator] ${result.valid ? '✓' : '✗'} Connector contract: `
|
|
565
|
+
+ `${result.valid ? 'PASS' : 'FAIL'} (${result.checked.length} checked: ${result.checked.join(', ') || 'none'})`);
|
|
566
|
+
return { valid: result.valid, errors: result.errors };
|
|
474
567
|
}
|
|
475
568
|
|
|
476
569
|
/**
|
|
@@ -515,7 +608,11 @@ class ValidationOrchestrator {
|
|
|
515
608
|
}
|
|
516
609
|
} else {
|
|
517
610
|
console.error(`[ValidationOrchestrator] ❌ Validation FAILED (${duration}ms)`);
|
|
518
|
-
|
|
611
|
+
// The trailing `(', ')}` was a leftover from a half-finished edit to a
|
|
612
|
+
// template literal: it printed on every failed validation as if it were
|
|
613
|
+
// part of the data. Found while reading this line for the biz-hello
|
|
614
|
+
// diagnosis (2026-08-29).
|
|
615
|
+
console.error(`[ValidationOrchestrator] Errors: ${JSON.stringify(results.errors, null, 2)}`);
|
|
519
616
|
}
|
|
520
617
|
|
|
521
618
|
return results;
|