@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
|
@@ -6,6 +6,23 @@ const crypto = require('crypto');
|
|
|
6
6
|
const MockMQClient = require('./mocks/MockMQClient');
|
|
7
7
|
const MockRegistry = require('./mocks/MockRegistry');
|
|
8
8
|
const { resolveHeaders } = require('./utils/resolveHeaders');
|
|
9
|
+
const { checkCookbookFormatVersion } = require('./utils/cookbookFormat');
|
|
10
|
+
const { getTestNamespace } = require('./utils/testNamespace');
|
|
11
|
+
const { describeStepFailure } = require('./utils/stepFailure');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The step's stopwatch, spelled out. A single total hid the fact that
|
|
15
|
+
* biz-hello's `count_stub` spent 2264 ms loading sequelize and ~0 ms doing its
|
|
16
|
+
* work; the reader of the log had no way to tell those apart, and neither did
|
|
17
|
+
* `expect.duration.max`.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} result - a step result carrying the three timings
|
|
20
|
+
* @returns {string} e.g. `2ms handler + 402ms module load = 404ms total`
|
|
21
|
+
*/
|
|
22
|
+
function describeStepTiming(result) {
|
|
23
|
+
return `${result.handlerDurationMs}ms handler + ${result.setupDurationMs}ms module load`
|
|
24
|
+
+ ` = ${result.duration}ms total`;
|
|
25
|
+
}
|
|
9
26
|
|
|
10
27
|
/**
|
|
11
28
|
* CookbookTestRunner — executes cookbook tests offline with mocked infrastructure.
|
|
@@ -17,7 +34,8 @@ const { resolveHeaders } = require('./utils/resolveHeaders');
|
|
|
17
34
|
*
|
|
18
35
|
* The handler dispatch builds a minimal real ctx:
|
|
19
36
|
* { logger, tenant_id, workspace_id, correlation_id, db: null, cache: null,
|
|
20
|
-
* httpClient: null, secrets: null,
|
|
37
|
+
* httpClient: null, secrets: null, state: null, stream: null,
|
|
38
|
+
* abortSignal: null }
|
|
21
39
|
* matching the shape ServiceWrapper.ContextBuilder produces in production
|
|
22
40
|
* (see api/docs/architecture/biz-service-invocation-model.md §5). Handlers
|
|
23
41
|
* that need DB access import sequelize directly from their own
|
|
@@ -78,8 +96,18 @@ class CookbookTestRunner {
|
|
|
78
96
|
? this.loadCookbook(cookbook)
|
|
79
97
|
: cookbook;
|
|
80
98
|
|
|
81
|
-
// Validate cookbook format
|
|
82
|
-
|
|
99
|
+
// Validate cookbook format. When the cookbook came from a file, the file
|
|
100
|
+
// path is appended to whatever the validation rejected — the reader of a
|
|
101
|
+
// boot failure otherwise learns that "a" cookbook is malformed but not
|
|
102
|
+
// which one, and a service can carry a dozen of them.
|
|
103
|
+
try {
|
|
104
|
+
this.validateCookbook(cookbookData);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (typeof cookbook === 'string') {
|
|
107
|
+
throw new Error(`${error.message} (cookbook file: ${cookbook})`, { cause: error });
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
83
111
|
|
|
84
112
|
// Get test configuration
|
|
85
113
|
const testConfig = cookbookData.test || { mode: 'production' };
|
|
@@ -94,9 +122,14 @@ class CookbookTestRunner {
|
|
|
94
122
|
const stepsArray = this.normalizeSteps(cookbookData.steps);
|
|
95
123
|
|
|
96
124
|
// Execute steps
|
|
125
|
+
const cookbookName = cookbookData.description || 'Unnamed';
|
|
97
126
|
const stepResults = [];
|
|
98
127
|
for (const step of stepsArray) {
|
|
99
128
|
const stepResult = await this.executeStep(step, testConfig);
|
|
129
|
+
// Which cookbook this step came from. Without it a failed step in the
|
|
130
|
+
// aggregate is an id with no file behind it, and a service can carry a
|
|
131
|
+
// dozen cookbooks (utils/stepFailure.js).
|
|
132
|
+
stepResult.cookbook = cookbookName;
|
|
100
133
|
stepResults.push(stepResult);
|
|
101
134
|
|
|
102
135
|
// If step failed and has expect, fail immediately
|
|
@@ -137,6 +170,21 @@ class CookbookTestRunner {
|
|
|
137
170
|
* @returns {Object} Aggregate results
|
|
138
171
|
*/
|
|
139
172
|
async runCookbooks(cookbooksDir) {
|
|
173
|
+
// Every run reports ITS OWN run. `this.results` only ever accumulated, and
|
|
174
|
+
// `ValidationOrchestrator` builds one runner in its constructor while
|
|
175
|
+
// `ServiceWrapper._ensureValidationProof` reuses one orchestrator across
|
|
176
|
+
// six startup retries — so a single transient failure on attempt 1 stayed
|
|
177
|
+
// in `failed` for the whole process, and step 4's verdict
|
|
178
|
+
// (`success: result.failed === 0`) could never come back true. Measured on
|
|
179
|
+
// biz-hello 2026-08-29: 7/8 → 15/16 → 23/24, then `Validation FAILED` in
|
|
180
|
+
// 104–221 ms while every step in the same log said PASSED — a healthy
|
|
181
|
+
// service reporting itself broken (automation-gates.md §5).
|
|
182
|
+
//
|
|
183
|
+
// resetResults() had a unit test and no production caller at all
|
|
184
|
+
// (change-discipline.md § "Removing something removes its declaration").
|
|
185
|
+
// This is that caller.
|
|
186
|
+
this.resetResults();
|
|
187
|
+
|
|
140
188
|
const files = fs.readdirSync(cookbooksDir).filter(f => f.endsWith('.json'));
|
|
141
189
|
|
|
142
190
|
this.logger.info(`Found ${files.length} cookbook(s) in ${cookbooksDir}`);
|
|
@@ -171,36 +219,49 @@ class CookbookTestRunner {
|
|
|
171
219
|
id: step.id,
|
|
172
220
|
operation: step.operation,
|
|
173
221
|
passed: false,
|
|
222
|
+
// Three separate numbers, because they answer different questions.
|
|
223
|
+
// `duration` is the wall clock of the whole step. `setupDurationMs` is
|
|
224
|
+
// what the RUNNER spent getting to the handler — reading operations.json
|
|
225
|
+
// and require()ing the module, a one-off cost per process. Only
|
|
226
|
+
// `handlerDurationMs` is the handler's own runtime, and only that one is
|
|
227
|
+
// what `expect.duration.max` asserts about (see validateExpectations).
|
|
174
228
|
duration: 0,
|
|
229
|
+
setupDurationMs: 0,
|
|
230
|
+
handlerDurationMs: 0,
|
|
175
231
|
request: null,
|
|
176
232
|
response: null,
|
|
177
233
|
expected: step.expect || null,
|
|
178
234
|
actual: null,
|
|
179
235
|
error: null,
|
|
236
|
+
errorStack: null,
|
|
180
237
|
validationErrors: []
|
|
181
238
|
};
|
|
182
239
|
|
|
183
240
|
try {
|
|
241
|
+
// The namespace is a safety boundary, so it is resolved before anything
|
|
242
|
+
// else happens — before the operation is even looked up. utils/testNamespace
|
|
243
|
+
// is the single owner of that decision: it reads the TESTING_* env contract
|
|
244
|
+
// (docs/biz/40-cookbooks/test-env-vars.md +
|
|
245
|
+
// docs/biz/60-templates/env-conventions.md — the TESTING_ prefix marks
|
|
246
|
+
// values that legitimately ship to production, because this runner fires at
|
|
247
|
+
// every service startup, prod included) AND it returns a namespace only for
|
|
248
|
+
// the non-production environment classes — 96 CI, 97 TESTING, 98 DEVEL,
|
|
249
|
+
// 99 VALIDATION (api/docs/standards/tenant-allocation.md). Production
|
|
250
|
+
// tenants, 100 LIVE and every customer from 101 up, are refused.
|
|
251
|
+
//
|
|
252
|
+
// That refusal is the runtime half of deploy-contract R8: R8 rejects a
|
|
253
|
+
// per-service override before it ships, this refuses the accident that
|
|
254
|
+
// ships anyway. Reading process.env here instead would route the startup
|
|
255
|
+
// probe straight around the guard — which is precisely how the 2026-08
|
|
256
|
+
// biz-property incident wrote into the live tenant at every restart.
|
|
257
|
+
// Values arrive already validated as integers, so ctx matches the
|
|
258
|
+
// production ContextBuilder for handlers checking Number.isInteger().
|
|
259
|
+
const { tenant_id: validationTenantId, workspace_id: validationWorkspaceId } = getTestNamespace();
|
|
260
|
+
|
|
184
261
|
// Resolve operation spec from operations.json — always returns
|
|
185
262
|
// { modulePath, exportName } (v3 handler dispatch is the only mode).
|
|
186
263
|
const spec = await this.resolveOperation(step.service, step.operation);
|
|
187
264
|
|
|
188
|
-
// Env-var contract (docs/biz/40-cookbooks/test-env-vars.md +
|
|
189
|
-
// docs/biz/60-templates/env-conventions.md): TESTING_* prefix marks
|
|
190
|
-
// values that legitimately ship to production because Tier-1 cookbook
|
|
191
|
-
// runner fires at every service startup, including in prod.
|
|
192
|
-
const rawTenantId = process.env.TESTING_TENANT_ID;
|
|
193
|
-
const rawWorkspaceId = process.env.TESTING_WORKSPACE_ID;
|
|
194
|
-
if (!rawTenantId || !rawWorkspaceId) {
|
|
195
|
-
throw new Error('[CookbookTestRunner] Missing required environment variables TESTING_TENANT_ID and/or TESTING_WORKSPACE_ID');
|
|
196
|
-
}
|
|
197
|
-
// Coerce numeric env vars to integers so ctx matches production
|
|
198
|
-
// ContextBuilder (which receives already-typed values from the MQ envelope).
|
|
199
|
-
// Handlers that check `Number.isInteger(ctx.tenant_id)` would otherwise
|
|
200
|
-
// reject the string form and every workspace-scoped cookbook would fail.
|
|
201
|
-
const validationTenantId = /^\d+$/.test(rawTenantId) ? parseInt(rawTenantId, 10) : rawTenantId;
|
|
202
|
-
const validationWorkspaceId = /^\d+$/.test(rawWorkspaceId) ? parseInt(rawWorkspaceId, 10) : rawWorkspaceId;
|
|
203
|
-
|
|
204
265
|
await this._dispatchViaHandler({
|
|
205
266
|
step,
|
|
206
267
|
spec,
|
|
@@ -211,14 +272,26 @@ class CookbookTestRunner {
|
|
|
211
272
|
startTime
|
|
212
273
|
});
|
|
213
274
|
|
|
214
|
-
|
|
275
|
+
if (result.passed) {
|
|
276
|
+
this.logger.info(`Step ${step.id}: PASSED (${describeStepTiming(result)})`);
|
|
277
|
+
} else {
|
|
278
|
+
// A verdict without its reason is a silent gate (automation-gates.md
|
|
279
|
+
// §5). Until 2026-08-29 this line said only "FAILED (1101ms)" and the
|
|
280
|
+
// reason — held in result.validationErrors or result.error — reached
|
|
281
|
+
// nobody, which made a failed biz-hello boot undiagnosable from logs.
|
|
282
|
+
this.logger.error(`Step ${step.id}: FAILED (${describeStepTiming(result)}) — ${describeStepFailure(result)}`);
|
|
283
|
+
}
|
|
215
284
|
|
|
216
285
|
} catch (error) {
|
|
217
286
|
result.error = error.message;
|
|
287
|
+
result.errorStack = error.stack;
|
|
218
288
|
result.duration = Date.now() - startTime;
|
|
289
|
+
// Nothing reached the handler, so all of it was setup.
|
|
290
|
+
result.setupDurationMs = result.duration;
|
|
291
|
+
result.handlerDurationMs = 0;
|
|
219
292
|
result.passed = false;
|
|
220
293
|
|
|
221
|
-
this.logger.error(`Step ${step.id}: ERROR - ${error.message}`);
|
|
294
|
+
this.logger.error(`Step ${step.id}: ERROR - ${error.message}\n${error.stack}`);
|
|
222
295
|
}
|
|
223
296
|
|
|
224
297
|
return result;
|
|
@@ -252,6 +325,14 @@ class CookbookTestRunner {
|
|
|
252
325
|
cache: null,
|
|
253
326
|
httpClient: null,
|
|
254
327
|
secrets: null,
|
|
328
|
+
// Persistent Redis state (conn-base-state), the slot production builds
|
|
329
|
+
// in ContextBuilder. Null here, and null is the only honest value:
|
|
330
|
+
// Tier-1 validation runs in ServiceWrapper phase 0.2, before
|
|
331
|
+
// _initializeState() instantiates the connector, so there is nothing to
|
|
332
|
+
// inject — same situation as db and cache. Declaring the slot keeps a
|
|
333
|
+
// handler that reads ctx.state from receiving `undefined`, which is a
|
|
334
|
+
// different failure from the documented `null`.
|
|
335
|
+
state: null,
|
|
255
336
|
stream: null,
|
|
256
337
|
abortSignal: null,
|
|
257
338
|
headers: {
|
|
@@ -298,6 +379,12 @@ class CookbookTestRunner {
|
|
|
298
379
|
if (timeoutId.unref) timeoutId.unref();
|
|
299
380
|
});
|
|
300
381
|
|
|
382
|
+
// Everything up to here was the runner getting ready: the namespace, the
|
|
383
|
+
// operations.json read, require.resolve and the require() of the handler
|
|
384
|
+
// module. That is a one-off cost per process and it is not the handler's.
|
|
385
|
+
const handlerStart = Date.now();
|
|
386
|
+
result.setupDurationMs = handlerStart - startTime;
|
|
387
|
+
|
|
301
388
|
let handlerError = null;
|
|
302
389
|
let handlerOutput = null;
|
|
303
390
|
try {
|
|
@@ -311,7 +398,9 @@ class CookbookTestRunner {
|
|
|
311
398
|
clearTimeout(timeoutId);
|
|
312
399
|
}
|
|
313
400
|
|
|
314
|
-
|
|
401
|
+
const finished = Date.now();
|
|
402
|
+
result.handlerDurationMs = finished - handlerStart;
|
|
403
|
+
result.duration = finished - startTime;
|
|
315
404
|
|
|
316
405
|
if (handlerError) {
|
|
317
406
|
// Synthesize an HTTP-like response so existing expect validators
|
|
@@ -327,6 +416,9 @@ class CookbookTestRunner {
|
|
|
327
416
|
};
|
|
328
417
|
result.actual = result.response.data;
|
|
329
418
|
result.error = { code: errorCode, message: handlerError.message };
|
|
419
|
+
// The stack is the half of a thrown error that says WHERE. Kept on the
|
|
420
|
+
// result so the step's failure line can print it (utils/stepFailure.js).
|
|
421
|
+
result.errorStack = handlerError.stack;
|
|
330
422
|
} else {
|
|
331
423
|
result.response = {
|
|
332
424
|
status: 200,
|
|
@@ -368,10 +460,23 @@ class CookbookTestRunner {
|
|
|
368
460
|
}
|
|
369
461
|
}
|
|
370
462
|
|
|
371
|
-
// Validate duration
|
|
463
|
+
// Validate duration — of the HANDLER.
|
|
464
|
+
//
|
|
465
|
+
// Until 2026-08-29 this compared `result.duration`, the whole step, whose
|
|
466
|
+
// dominant term for any handler with a heavy module is the one-off
|
|
467
|
+
// require(). biz-hello's `count_stub` is a stub returning four constants;
|
|
468
|
+
// its module pulls in sequelize, measured at 2264 ms cold inside the
|
|
469
|
+
// container against a 1000 ms bar, while every other cookbook handler in
|
|
470
|
+
// the same service loaded in 5–25 ms. So the boot verdict of a healthy
|
|
471
|
+
// service depended on filesystem cache warmth — a gate that is not
|
|
472
|
+
// predictable is not a gate (automation-gates.md §1.1).
|
|
473
|
+
//
|
|
474
|
+
// The load cost is not swept away: it is `setupDurationMs`, it is printed
|
|
475
|
+
// on the step's log line, and it stays inside `duration`.
|
|
372
476
|
if (expect.duration && expect.duration.max) {
|
|
373
|
-
if (result.
|
|
374
|
-
errors.push(`
|
|
477
|
+
if (result.handlerDurationMs > expect.duration.max) {
|
|
478
|
+
errors.push(`Handler duration ${result.handlerDurationMs}ms exceeds max ${expect.duration.max}ms`
|
|
479
|
+
+ ` (module load ${result.setupDurationMs}ms is the runner's own cost and is not counted)`);
|
|
375
480
|
}
|
|
376
481
|
}
|
|
377
482
|
|
|
@@ -573,7 +678,13 @@ class CookbookTestRunner {
|
|
|
573
678
|
}
|
|
574
679
|
|
|
575
680
|
const content = fs.readFileSync(cookbookPath, 'utf8');
|
|
576
|
-
|
|
681
|
+
try {
|
|
682
|
+
return JSON.parse(content);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
throw new Error(`[CookbookTestRunner] Cookbook file is not valid JSON - Expected a JSON object per `
|
|
685
|
+
+ `api/docs/biz/40-cookbooks/format.md. Fix: repair the JSON syntax in ${cookbookPath} (${error.message})`,
|
|
686
|
+
{ cause: error });
|
|
687
|
+
}
|
|
577
688
|
}
|
|
578
689
|
|
|
579
690
|
/**
|
|
@@ -605,8 +716,26 @@ class CookbookTestRunner {
|
|
|
605
716
|
/**
|
|
606
717
|
* Validate cookbook format
|
|
607
718
|
* Supports both V1 (steps as array) and V2 (steps as object) formats
|
|
719
|
+
*
|
|
720
|
+
* The format-version check is FIRST and unconditional. Until 2026-08-27 it
|
|
721
|
+
* lived only in `CookbookTestUtils.validateCookbook`, reachable exclusively
|
|
722
|
+
* through the optional `tests/bootstrap/service-readiness.test.js` wrapper
|
|
723
|
+
* that 3 of 8 biz repos happen to have — so the rule from
|
|
724
|
+
* api/docs/biz/40-cookbooks/format.md § "Required fields" was, in practice,
|
|
725
|
+
* opt-in per repo, and 4 repos ship cookbooks with no `version` field at all.
|
|
726
|
+
* A check a repo can decline is not a check (automation-gates §1.5).
|
|
608
727
|
*/
|
|
609
728
|
validateCookbook(cookbook) {
|
|
729
|
+
if (!cookbook || typeof cookbook !== 'object' || Array.isArray(cookbook)) {
|
|
730
|
+
throw new Error('[CookbookTestRunner] Cookbook is not a JSON object - Expected an object with "version" and "steps". '
|
|
731
|
+
+ 'Fix: check the cookbook file content (api/docs/biz/40-cookbooks/format.md § Basic structure).');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const versionProblem = checkCookbookFormatVersion(cookbook.version);
|
|
735
|
+
if (versionProblem) {
|
|
736
|
+
throw new Error(`[CookbookTestRunner] ${versionProblem}`);
|
|
737
|
+
}
|
|
738
|
+
|
|
610
739
|
if (!cookbook.steps || (typeof cookbook.steps !== 'object' && !Array.isArray(cookbook.steps))) {
|
|
611
740
|
throw new Error('Cookbook must have steps (array or object)');
|
|
612
741
|
}
|
package/src/CookbookTestUtils.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { MIN_COOKBOOK_FORMAT_VERSION, checkCookbookFormatVersion } = require('./utils/cookbookFormat');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* CookbookTestUtils - Utilities for cookbook testing
|
|
5
7
|
*/
|
|
@@ -11,7 +13,7 @@ class CookbookTestUtils {
|
|
|
11
13
|
return {
|
|
12
14
|
workflow_id: options.workflow_id || `test-workflow-${Date.now()}`,
|
|
13
15
|
cookbook: options.cookbook || {
|
|
14
|
-
version:
|
|
16
|
+
version: MIN_COOKBOOK_FORMAT_VERSION,
|
|
15
17
|
steps: options.steps || [
|
|
16
18
|
{
|
|
17
19
|
id: 'step1',
|
|
@@ -51,7 +53,7 @@ class CookbookTestUtils {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
return {
|
|
54
|
-
version:
|
|
56
|
+
version: MIN_COOKBOOK_FORMAT_VERSION,
|
|
55
57
|
api_input: options.api_input || { test: 'data' },
|
|
56
58
|
steps
|
|
57
59
|
};
|
|
@@ -59,6 +61,11 @@ class CookbookTestUtils {
|
|
|
59
61
|
|
|
60
62
|
/**
|
|
61
63
|
* Validate cookbook structure (basic validation)
|
|
64
|
+
*
|
|
65
|
+
* The format-version rule is the shared one (`utils/cookbookFormat`), the
|
|
66
|
+
* same rule the Tier-1 `CookbookTestRunner` enforces — one fact, one owner.
|
|
67
|
+
* Presence alone used to be enough here, so a cookbook stuck on an outdated
|
|
68
|
+
* format passed the readiness wrapper unnoticed.
|
|
62
69
|
*/
|
|
63
70
|
static validateCookbook(cookbook) {
|
|
64
71
|
const errors = [];
|
|
@@ -68,8 +75,9 @@ class CookbookTestUtils {
|
|
|
68
75
|
return { valid: false, errors };
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
const versionProblem = checkCookbookFormatVersion(cookbook.version);
|
|
79
|
+
if (versionProblem) {
|
|
80
|
+
errors.push(versionProblem);
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
if (!cookbook.steps || !Array.isArray(cookbook.steps)) {
|
|
@@ -21,6 +21,17 @@ const CookbookTestUtils = require('./CookbookTestUtils');
|
|
|
21
21
|
* previously spent on `health` fold into `operations` so total stays
|
|
22
22
|
* at 100.
|
|
23
23
|
*
|
|
24
|
+
* 2026-08-22: the `url` the probe consumed is gone too. It outlived the
|
|
25
|
+
* probe by five months as an accepted-and-ignored argument echoed into
|
|
26
|
+
* `results.serviceUrl` and printed as a `URL:` line — a report field
|
|
27
|
+
* naming an endpoint that does not exist. The only caller that still
|
|
28
|
+
* passed one was Tier-1's readiness step, and that step is gone
|
|
29
|
+
* (see ValidationOrchestrator: its verdict was a strict function of
|
|
30
|
+
* steps 2 and 3). What is left has exactly one consumer:
|
|
31
|
+
* `helpers/createServiceReadinessTests`, and through it the
|
|
32
|
+
* `tests/bootstrap/` suites of the biz repos — which pass name, version,
|
|
33
|
+
* operations, testCookbook and registry, and never passed a url.
|
|
34
|
+
*
|
|
24
35
|
* @see /api/docs/architecture/biz-service-invocation-model.md §5.3
|
|
25
36
|
* @see /api/docs/biz/40-cookbooks/test-runner-flow.md (input probe contract)
|
|
26
37
|
* @see /api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md
|
|
@@ -55,7 +66,6 @@ class ServiceReadinessValidator {
|
|
|
55
66
|
*/
|
|
56
67
|
async validateReadiness(service) {
|
|
57
68
|
const {
|
|
58
|
-
url,
|
|
59
69
|
operations,
|
|
60
70
|
registry,
|
|
61
71
|
testCookbook
|
|
@@ -64,7 +74,6 @@ class ServiceReadinessValidator {
|
|
|
64
74
|
const results = {
|
|
65
75
|
timestamp: Date.now(),
|
|
66
76
|
serviceName: service.name,
|
|
67
|
-
serviceUrl: url,
|
|
68
77
|
checks: {},
|
|
69
78
|
score: 0,
|
|
70
79
|
maxScore: 100,
|
|
@@ -73,14 +82,29 @@ class ServiceReadinessValidator {
|
|
|
73
82
|
warnings: []
|
|
74
83
|
};
|
|
75
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Record what a check contributed. `createServiceReadinessTests` prints
|
|
87
|
+
* `checkResult.score` per check; nothing ever assigned it, so every biz
|
|
88
|
+
* bootstrap run printed "0 points" beside checks that had just been
|
|
89
|
+
* awarded their full weight, under a total that contradicted them.
|
|
90
|
+
*/
|
|
91
|
+
const award = (name, failureMessage) => {
|
|
92
|
+
const { weight, required } = this.checks[name];
|
|
93
|
+
if (results.checks[name].passed) {
|
|
94
|
+
results.checks[name].score = weight;
|
|
95
|
+
results.score += weight;
|
|
96
|
+
} else {
|
|
97
|
+
results.checks[name].score = 0;
|
|
98
|
+
if (required) {
|
|
99
|
+
results.errors.push(failureMessage);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
76
104
|
// 1. Validate operations.json structure
|
|
77
105
|
if (operations) {
|
|
78
106
|
results.checks.operations = await this.checkOperationsCompliance(operations);
|
|
79
|
-
|
|
80
|
-
results.score += this.checks.operations.weight;
|
|
81
|
-
} else if (this.checks.operations.required) {
|
|
82
|
-
results.errors.push('Operations validation failed');
|
|
83
|
-
}
|
|
107
|
+
award('operations', 'Operations validation failed');
|
|
84
108
|
} else {
|
|
85
109
|
results.errors.push('No operations.json provided - required for all services');
|
|
86
110
|
results.checks.operations = { passed: false, error: 'Missing operations.json' };
|
|
@@ -101,11 +125,7 @@ class ServiceReadinessValidator {
|
|
|
101
125
|
results.checks.cookbook = await this.checkCookbookExecution(
|
|
102
126
|
testCookbook
|
|
103
127
|
);
|
|
104
|
-
|
|
105
|
-
results.score += this.checks.cookbook.weight;
|
|
106
|
-
} else if (this.checks.cookbook.required) {
|
|
107
|
-
results.errors.push('Cookbook validation failed');
|
|
108
|
-
}
|
|
128
|
+
award('cookbook', 'Cookbook validation failed');
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
// 5. Verify registry compatibility
|
|
@@ -114,11 +134,7 @@ class ServiceReadinessValidator {
|
|
|
114
134
|
service,
|
|
115
135
|
registry
|
|
116
136
|
);
|
|
117
|
-
|
|
118
|
-
results.score += this.checks.registry.weight;
|
|
119
|
-
} else if (this.checks.registry.required) {
|
|
120
|
-
results.errors.push('Registry compatibility check failed');
|
|
121
|
-
}
|
|
137
|
+
award('registry', 'Registry compatibility check failed');
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
// Calculate readiness
|
|
@@ -292,7 +308,8 @@ class ServiceReadinessValidator {
|
|
|
292
308
|
|
|
293
309
|
report.push('=== Service Readiness Report ===');
|
|
294
310
|
report.push(`Service: ${results.serviceName}`);
|
|
295
|
-
|
|
311
|
+
// No URL line: biz services expose no endpoint (ADR 0005). Printing one
|
|
312
|
+
// named an address nothing listened on.
|
|
296
313
|
report.push(`Score: ${results.score}/${results.maxScore}`);
|
|
297
314
|
report.push(`Status: ${results.ready ? 'READY' : 'NOT READY'}`);
|
|
298
315
|
report.push('');
|