@onlineapps/conn-orch-validator 12.1.1 → 13.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 +607 -0
- package/README.md +126 -19
- package/manifests/biz-service.manifest.json +15 -2
- package/manifests/library.manifest.json +4 -4
- package/package.json +11 -3
- package/src/CookbookTestRunner.js +275 -105
- package/src/CookbookTestUtils.js +79 -68
- package/src/ServiceReadinessValidator.js +42 -52
- package/src/ValidationOrchestrator.js +65 -44
- package/src/cli/biz-ci-gate.js +2 -2
- package/src/cli/oa-sync-template.js +97 -47
- package/src/cli/oa-validate.js +44 -10
- package/src/helpers/README.md +6 -6
- package/src/helpers/createServiceReadinessTests.js +87 -33
- package/src/index.js +14 -5
- package/src/lint/scripts/lintScripts.js +11 -4
- package/src/manifest/checks/libraryContext.js +6 -3
- package/src/manifest/checks/libraryDocs.js +174 -4
- package/src/manifest/checks/libraryTests.js +200 -19
- package/src/manifest/checks/scriptHeaders.js +6 -13
- package/src/manifest/checks/serviceConfig.js +36 -16
- package/src/manifest/checks/serviceConnectors.js +180 -2
- package/src/manifest/checks/serviceDb.js +0 -3
- package/src/manifest/checks/serviceScripts.js +3 -20
- package/src/manifest/runManifest.js +90 -13
- package/src/manifest/workspaceRoot.js +133 -4
- package/src/mocks/MockMQClient.js +2 -2
- package/src/sync/docsRegion.js +2 -2
- package/src/sync/readmeFile.js +30 -0
- package/src/sync/readmeLocation.js +2 -12
- package/src/sync/readmePointer.js +10 -4
- package/src/sync/serviceTemplate.js +9 -11
- package/src/sync/sharedEnv.js +59 -3
- package/src/sync/uniformFiles.js +81 -8
- package/src/utils/bizCiGateContract.js +2 -2
- package/src/utils/connectorContract.js +54 -2
- package/src/utils/cookbookFormat.js +25 -115
- package/src/utils/dbAccountGrants.js +5 -3
- package/src/utils/deployContract.js +153 -28
- package/src/utils/envContract.js +2 -2
- package/src/utils/handlerRef.js +8 -10
- package/src/utils/integrationRun.js +1 -1
- package/src/utils/operationsDocumentRules.js +242 -0
- package/src/utils/operationsRules.js +157 -0
- package/src/utils/resolveHeaders.js +12 -1
- package/src/utils/setupDatabase.js +1 -1
- package/src/utils/stepFailure.js +3 -3
- package/src/utils/stepReferences.js +28 -87
- package/src/utils/throwawaySchema.js +1 -1
- package/src/utils/yamlTopLevel.js +105 -0
- package/src/validators/ServiceStructureValidator.js +67 -152
- package/templates/business-service/.gitlab-ci.yml +203 -37
- package/templates/business-service/README.md +7 -4
- package/templates/business-service/config/env-templates/shared.env +1 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +31 -3
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +5 -2
- package/templates/business-service/src/config/index.js +15 -0
- package/TESTING_STRATEGY.md +0 -92
- package/jest.config.js +0 -37
|
@@ -8,20 +8,36 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Four runs, one template:
|
|
10
10
|
*
|
|
11
|
-
* * bare paths - the uniform sync of confirmation 001 §3.2
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* * bare paths - the uniform sync of confirmation 001 §3.2, and since d.710
|
|
12
|
+
* the WHOLE of it: over a service root with no path named it writes every
|
|
13
|
+
* row the manifest declares in a syncable class - `identical`, `generated`,
|
|
14
|
+
* `contains` - from the very `from:` reference those rows already point the
|
|
15
|
+
* conformance CHECK at, and never touches a file of the `own` class. That
|
|
16
|
+
* is one run for "bring this service into line with everything oa-validate
|
|
17
|
+
* measures as generated", `config/env-templates/shared.env` included. A row
|
|
18
|
+
* that names no `from:` reference is not a row of this run — it is a
|
|
19
|
+
* requirement about the repository, and `npx oa-validate` is what answers
|
|
20
|
+
* it (d.507).
|
|
17
21
|
* * `--new <name>` - the whole service tree, from the template that ships
|
|
18
22
|
* inside this package (001 §2). It is what `api/scripts/add-service.sh
|
|
19
23
|
* --scaffold` runs; that script keeps the platform facts (the registry
|
|
20
24
|
* entry, the port), because those are not the shape of a service.
|
|
21
25
|
* * `shared-env` - `config/env-templates/shared.env` from
|
|
22
26
|
* `api/config/shared-env.json`, the one owner of the shared key set (003 §18).
|
|
27
|
+
* It stays, because its bearer is not always a service: `api/` itself and
|
|
28
|
+
* the business-service template carry that file too, and neither is a
|
|
29
|
+
* service root the bare run could be pointed at. Both of those paths are
|
|
30
|
+
* READ OFF the manifest row (`sharedEnvRow()` below) rather than written
|
|
31
|
+
* here, so this command and the row cannot disagree about one file.
|
|
23
32
|
* * `readme-uniform` - the generated uniform pointer of a README, for a
|
|
24
|
-
* library or for a biz service; the kind is read from the target (005).
|
|
33
|
+
* library or for a biz service; the kind is read from the target (005). It
|
|
34
|
+
* stays for the LIBRARY, which wears no service uniform; for a service the
|
|
35
|
+
* bare run covers the same region through row `G-README`.
|
|
36
|
+
*
|
|
37
|
+
* `template` and `docs-region` are deliberately NOT part of the bare run: their
|
|
38
|
+
* bearer is not a service. One renders `api/templates/business-service`, the
|
|
39
|
+
* other a region of the `api/docs/**` tree — two directories a service
|
|
40
|
+
* repository does not have and does not own.
|
|
25
41
|
*
|
|
26
42
|
* There is no flag that points the run at another manifest: the shape has one
|
|
27
43
|
* owner beside the SSOT (004), and a `--manifest` flag would be a second one
|
|
@@ -57,8 +73,8 @@ const {
|
|
|
57
73
|
declaredCategory,
|
|
58
74
|
KINDS
|
|
59
75
|
} = require('../sync/readmePointer');
|
|
76
|
+
const { README_FILE } = require('../sync/readmeFile');
|
|
60
77
|
const {
|
|
61
|
-
README_FILE,
|
|
62
78
|
PACKAGE_ROOT,
|
|
63
79
|
SERVICE_MANIFEST_IN_PACKAGE,
|
|
64
80
|
LIBRARY_MANIFEST_IN_PACKAGE,
|
|
@@ -67,27 +83,30 @@ const {
|
|
|
67
83
|
libraryRegion
|
|
68
84
|
} = require('../sync/readmeLocation');
|
|
69
85
|
const docsRegion = require('../sync/docsRegion');
|
|
70
|
-
const {
|
|
86
|
+
const {
|
|
87
|
+
resolveWorkspaceRoot, resolveWorkspacePath, canonicalRoot, WORKSPACE_MARKER
|
|
88
|
+
} = require('../manifest/workspaceRoot');
|
|
71
89
|
const { loadManifest: loadLibraryManifest, LIBRARY_MANIFEST_PATH } = require('../manifest/loadManifest');
|
|
72
|
-
const { discoverBearers } = require('../manifest/discovery');
|
|
90
|
+
const { discoverBearers, resolveFromDocument } = require('../manifest/discovery');
|
|
73
91
|
|
|
74
92
|
/** Exit code for a run that could not start at all — distinct from a finding. */
|
|
75
93
|
const USAGE_EXIT = 2;
|
|
76
94
|
|
|
77
95
|
/**
|
|
78
|
-
* The
|
|
79
|
-
*
|
|
80
|
-
*
|
|
96
|
+
* The row that owns `shared.env`, found by the CHECK that decides it.
|
|
97
|
+
*
|
|
98
|
+
* By the check and not by the id, because that is already this package's
|
|
99
|
+
* vocabulary for "which rule renders this row" (`../sync/uniformFiles.js` §
|
|
100
|
+
* `RENDERERS` is keyed the same way). A list of ids in a runner is the thing
|
|
101
|
+
* that module refuses to keep: a row is renderable because its rule says how,
|
|
102
|
+
* never because a caller recognised its name.
|
|
81
103
|
*/
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
/** Where a bearer keeps the file this subcommand generates. */
|
|
85
|
-
const SHARED_ENV_RELATIVE = path.join('config', 'env-templates', 'shared.env');
|
|
86
|
-
|
|
87
|
-
/** The same file as a tree key, which is always '/'-separated. */
|
|
88
|
-
const SHARED_ENV_POSIX = 'config/env-templates/shared.env';
|
|
104
|
+
const SHARED_ENV_CHECK = 'shared-env-generated';
|
|
89
105
|
|
|
90
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* The platform SSOT of library versions, in the same convention as the manifest
|
|
108
|
+
* above and resolved the same way.
|
|
109
|
+
*/
|
|
91
110
|
const LIBRARIES_RELATIVE = path.join(path.dirname(WORKSPACE_MARKER), 'libraries.json');
|
|
92
111
|
|
|
93
112
|
/** The scope prefix whose versions the SSOT owns. */
|
|
@@ -108,21 +127,31 @@ Usage:
|
|
|
108
127
|
is: generated output, committed for reading (001 §5). --check is the gate.
|
|
109
128
|
|
|
110
129
|
With bare paths (or none, meaning all of them) the run rewrites the files the
|
|
111
|
-
manifest declares in the classes "identical" and "
|
|
112
|
-
"from" reference, from that same reference the conformance check reads.
|
|
113
|
-
of the "own" class - src/**, tests/**, the content of docs/** - are
|
|
114
|
-
written. A row naming no reference is a requirement about the repository
|
|
130
|
+
manifest declares in the classes "identical", "generated" and "contains" that
|
|
131
|
+
name a "from" reference, from that same reference the conformance check reads.
|
|
132
|
+
Files of the "own" class - src/**, tests/**, the content of docs/** - are
|
|
133
|
+
never written. A row naming no reference is a requirement about the repository
|
|
115
134
|
rather than a file, and npx oa-validate <serviceRoot> is what answers it.
|
|
116
135
|
|
|
136
|
+
WHAT ONE BARE RUN COVERS: every generated row of the SERVICE uniform, which
|
|
137
|
+
since d.710 includes config/env-templates/shared.env and the README region -
|
|
138
|
+
so "sync everything that is generated" is npx oa-sync-template --target .,
|
|
139
|
+
one command, not a sequence each repository assembles for itself. WHAT IT
|
|
140
|
+
DOES NOT: template and docs-region, whose bearer is not a service -
|
|
141
|
+
api/templates/business-service and the api/docs/** tree have their own owners.
|
|
142
|
+
|
|
117
143
|
--new <name> create a service tree from the packaged template
|
|
118
144
|
--into <dir> where that tree is written; it must not exist yet
|
|
119
145
|
--description <s> what the service is for; without it the template's
|
|
120
146
|
__SERVICE_DESCRIPTION__ is LEFT standing and the files
|
|
121
147
|
carrying it are listed
|
|
122
148
|
|
|
123
|
-
|
|
124
|
-
api/config/shared-env.json — the one owner of the shared key set. A
|
|
125
|
-
adds keys of its own in service.env, never here.
|
|
149
|
+
shared-env writes <dir>/config/env-templates/shared.env from the platform env
|
|
150
|
+
manifest api/config/shared-env.json — the one owner of the shared key set. A
|
|
151
|
+
service adds keys of its own in service.env, never here. For a SERVICE the
|
|
152
|
+
bare run writes the same file from the same manifest (row G-SHARED-ENV); this
|
|
153
|
+
subcommand is how the other two bearers get it — api/ itself and the
|
|
154
|
+
business-service template, neither of which is a service root.
|
|
126
155
|
|
|
127
156
|
--target <dir> the bearer: a service root, api/ itself, or the business
|
|
128
157
|
service template
|
|
@@ -131,7 +160,7 @@ Usage:
|
|
|
131
160
|
--check write nothing; exit 1 with a diff when the file on disk is
|
|
132
161
|
not what the manifest renders
|
|
133
162
|
|
|
134
|
-
readme-uniform writes the generated region of a repository's
|
|
163
|
+
readme-uniform writes the generated region of a repository's ${README_FILE} from
|
|
135
164
|
the manifest that ships with this package. --target reads the kind from the
|
|
136
165
|
disk: a root carrying config/service/operations.json is a SERVICE and gets
|
|
137
166
|
"Uniform: [biz-service](<link>)", the sections of the biz-service manifest and
|
|
@@ -257,18 +286,30 @@ function parseArgs(argv) {
|
|
|
257
286
|
return options;
|
|
258
287
|
}
|
|
259
288
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
289
|
+
/**
|
|
290
|
+
* The manifest row that owns `shared.env`: where a bearer keeps the file
|
|
291
|
+
* (`row.path`) and which file owns its content (`row.from`).
|
|
292
|
+
*
|
|
293
|
+
* BOTH facts come from the row, and that is the whole point of this function.
|
|
294
|
+
* Until d.710c this command carried its own pair — a `MANIFEST_RELATIVE` built
|
|
295
|
+
* from the workspace marker and a `SHARED_ENV_RELATIVE` literal — beside the
|
|
296
|
+
* row that says the same two things to `oa-validate` and, since d.710, to the
|
|
297
|
+
* bare run. One file, two declarations of where it lives: the day the row's
|
|
298
|
+
* `from.path` moves, the subcommand keeps reading the old place and the two
|
|
299
|
+
* halves of one uniform disagree in silence
|
|
300
|
+
* (`.claude/rules/change-discipline.md` § One rail per concern).
|
|
301
|
+
*
|
|
302
|
+
* @returns {object} the row
|
|
303
|
+
*/
|
|
304
|
+
function sharedEnvRow() {
|
|
305
|
+
const manifest = loadUniformManifest(DEFAULT_MANIFEST_PATH);
|
|
306
|
+
const row = uniformRows(manifest).find((candidate) => candidate.check === SHARED_ENV_CHECK);
|
|
307
|
+
if (row === undefined) {
|
|
308
|
+
throw new Error(`[oa-sync-template] No manifest row is decided by "${SHARED_ENV_CHECK}" - this run writes `
|
|
309
|
+
+ 'the file that row declares, and knows no path of its own. '
|
|
310
|
+
+ `Fix: restore the row in ${DEFAULT_MANIFEST_PATH}.`);
|
|
271
311
|
}
|
|
312
|
+
return row;
|
|
272
313
|
}
|
|
273
314
|
|
|
274
315
|
/**
|
|
@@ -287,8 +328,9 @@ function runSharedEnv(options) {
|
|
|
287
328
|
+ 'Fix: pass --workspace <root> pointing at the directory that holds api/ and api_biz/.');
|
|
288
329
|
}
|
|
289
330
|
|
|
290
|
-
const
|
|
291
|
-
const
|
|
331
|
+
const row = sharedEnvRow();
|
|
332
|
+
const manifest = resolveFromDocument({ from: row.from, workspaceRoot });
|
|
333
|
+
const file = path.join(target, ...row.path.split('/'));
|
|
292
334
|
|
|
293
335
|
if (options.check) {
|
|
294
336
|
if (!fs.existsSync(file)) {
|
|
@@ -765,7 +807,14 @@ function runSync(options) {
|
|
|
765
807
|
let blocked = 0;
|
|
766
808
|
|
|
767
809
|
for (const entry of entries) {
|
|
768
|
-
|
|
810
|
+
// WHAT THE LINE IS ABOUT, not merely which file it touched. A block row
|
|
811
|
+
// compares and writes the markers alone, so a line naming the path only
|
|
812
|
+
// reads as a claim about a file whose other half was never looked at —
|
|
813
|
+
// `.claude/rules/automation-gates.md` §5. `planRow` decides the scope, by
|
|
814
|
+
// the manifest row's own `block`; this loop only prints it.
|
|
815
|
+
const where = entry.block === undefined
|
|
816
|
+
? `${entry.id} ${entry.path}`
|
|
817
|
+
: `${entry.id} ${entry.path} (block "${entry.block}")`;
|
|
769
818
|
if (entry.outcome === 'not-run') {
|
|
770
819
|
process.stdout.write(`[oa-sync-template] NOT RUN ${where} - ${entry.reason}\n`);
|
|
771
820
|
} else if (entry.outcome === 'blocked') {
|
|
@@ -797,7 +846,7 @@ function runSync(options) {
|
|
|
797
846
|
* @returns {object} the parsed `libraries` map
|
|
798
847
|
*/
|
|
799
848
|
function loadLibraries(workspaceRoot) {
|
|
800
|
-
const file =
|
|
849
|
+
const file = resolveWorkspacePath(workspaceRoot, LIBRARIES_RELATIVE);
|
|
801
850
|
if (!fs.existsSync(file)) {
|
|
802
851
|
throw new Error(`[oa-sync-template] Missing library SSOT - ${file} does not exist. `
|
|
803
852
|
+ 'Fix: a new service pins every @onlineapps dependency to the platform SSOT, which is that file.');
|
|
@@ -899,7 +948,8 @@ function runNew(options) {
|
|
|
899
948
|
}
|
|
900
949
|
|
|
901
950
|
const workspaceRoot = requireWorkspaceRoot({ explicit: options.workspace, startDir: path.dirname(into) });
|
|
902
|
-
const
|
|
951
|
+
const sharedEnv = sharedEnvRow();
|
|
952
|
+
const envManifest = resolveFromDocument({ from: sharedEnv.from, workspaceRoot });
|
|
903
953
|
const libraries = loadLibraries(workspaceRoot);
|
|
904
954
|
|
|
905
955
|
const tree = renderTree({ params });
|
|
@@ -908,7 +958,7 @@ function runNew(options) {
|
|
|
908
958
|
tree.delete(ENV_TEMPLATE_SOURCE);
|
|
909
959
|
tree.set(envTemplateTarget(params.service_name), envTemplate);
|
|
910
960
|
|
|
911
|
-
tree.set(
|
|
961
|
+
tree.set(sharedEnv.path, renderSharedEnv(envManifest));
|
|
912
962
|
tree.set('package.json', pinLibraries(tree.get('package.json'), libraries));
|
|
913
963
|
|
|
914
964
|
for (const [relative, text] of tree) writeServiceFile(into, relative, text);
|
|
@@ -1032,4 +1082,4 @@ if (require.main === module) {
|
|
|
1032
1082
|
process.exitCode = main(process.argv.slice(2));
|
|
1033
1083
|
}
|
|
1034
1084
|
|
|
1035
|
-
module.exports = { main, parseArgs, USAGE_EXIT
|
|
1085
|
+
module.exports = { main, parseArgs, USAGE_EXIT };
|
package/src/cli/oa-validate.js
CHANGED
|
@@ -30,9 +30,9 @@ const fs = require('fs');
|
|
|
30
30
|
const path = require('path');
|
|
31
31
|
|
|
32
32
|
const { loadManifest, DEFAULT_MANIFEST_PATH, LIBRARY_MANIFEST_PATH } = require('../manifest/loadManifest');
|
|
33
|
-
const { runManifest } = require('../manifest/runManifest');
|
|
33
|
+
const { runManifest, isTargetVersion } = require('../manifest/runManifest');
|
|
34
34
|
const {
|
|
35
|
-
resolveWorkspaceRoot, canonicalRoot, API_MARKER, PACKAGE_ROOT
|
|
35
|
+
resolveWorkspaceRoot, requirePackageInApiCheckout, canonicalRoot, API_MARKER, PACKAGE_ROOT
|
|
36
36
|
} = require('../manifest/workspaceRoot');
|
|
37
37
|
const {
|
|
38
38
|
renderReport, renderJson, exitCodeFor, blockingOf, verdictOf, describeClearOutcome
|
|
@@ -83,7 +83,7 @@ const USAGE = `
|
|
|
83
83
|
Usage:
|
|
84
84
|
oa-validate [serviceRoot] [--workspace <root>] [--json]
|
|
85
85
|
oa-validate --workspace <root> [--json]
|
|
86
|
-
oa-validate --library <packageDir> [--workspace <root>] [--json]
|
|
86
|
+
oa-validate --library <packageDir> [--workspace <root>] [--as-version <X.Y.Z>] [--json]
|
|
87
87
|
oa-validate --library --all [--workspace <root>] [--json]
|
|
88
88
|
oa-validate --env-reads [<serviceRoot>]
|
|
89
89
|
|
|
@@ -121,6 +121,11 @@ Options:
|
|
|
121
121
|
No such checkout (an installed copy, a service container)
|
|
122
122
|
→ the rows that need it are reported NOT RUN, never as
|
|
123
123
|
passing.
|
|
124
|
+
--as-version <X.Y.Z> With --library <packageDir>: judge the package AS the version
|
|
125
|
+
being published, not the one package.json still carries —
|
|
126
|
+
what a publish dry run asks before the wave bumps the file.
|
|
127
|
+
The rows that judge a version (L-CHANGELOG) read it; the
|
|
128
|
+
finding names it and the package.json version beside it.
|
|
124
129
|
--json Print the run as JSON instead of the table.
|
|
125
130
|
--env-reads Print the environment names this service reads — one per
|
|
126
131
|
line, sorted, nothing else — and exit 0. No table, no
|
|
@@ -148,7 +153,8 @@ Exit codes:
|
|
|
148
153
|
|
|
149
154
|
function parseArgs(argv) {
|
|
150
155
|
const parsed = {
|
|
151
|
-
serviceRoot: null, workspace: null, json: false, help: false, library: false, all: false, envReads: false
|
|
156
|
+
serviceRoot: null, workspace: null, json: false, help: false, library: false, all: false, envReads: false,
|
|
157
|
+
asVersion: null
|
|
152
158
|
};
|
|
153
159
|
const args = [...argv];
|
|
154
160
|
|
|
@@ -171,6 +177,17 @@ function parseArgs(argv) {
|
|
|
171
177
|
+ 'api/ and api_biz/. Fix: oa-validate [serviceRoot] --workspace <root>');
|
|
172
178
|
}
|
|
173
179
|
parsed.workspace = value;
|
|
180
|
+
} else if (token === '--as-version') {
|
|
181
|
+
const value = args.shift();
|
|
182
|
+
if (!value || value.startsWith('--')) {
|
|
183
|
+
throw new Error('[oa-validate] Option --as-version has no value - it names the version being '
|
|
184
|
+
+ 'published. Fix: oa-validate --library <packageDir> --as-version <X.Y.Z>');
|
|
185
|
+
}
|
|
186
|
+
if (!isTargetVersion(value)) {
|
|
187
|
+
throw new Error(`[oa-validate] Option --as-version is not a version - ${JSON.stringify(value)} is not `
|
|
188
|
+
+ 'X.Y.Z or X.Y.Z-<prerelease>. Fix: pass the bare version, e.g. --as-version 1.2.4');
|
|
189
|
+
}
|
|
190
|
+
parsed.asVersion = value;
|
|
174
191
|
} else if (token.startsWith('--')) {
|
|
175
192
|
throw new Error(`[oa-validate] Unknown option - ${token}. Fix: run oa-validate --help for the options.`);
|
|
176
193
|
} else if (parsed.serviceRoot === null) {
|
|
@@ -192,7 +209,8 @@ function parseArgs(argv) {
|
|
|
192
209
|
parsed.json ? '--json' : null,
|
|
193
210
|
parsed.library ? '--library' : null,
|
|
194
211
|
parsed.all ? '--all' : null,
|
|
195
|
-
parsed.workspace !== null ? '--workspace' : null
|
|
212
|
+
parsed.workspace !== null ? '--workspace' : null,
|
|
213
|
+
parsed.asVersion !== null ? '--as-version' : null
|
|
196
214
|
].filter((token) => token !== null);
|
|
197
215
|
|
|
198
216
|
if (combined.length > 0) {
|
|
@@ -202,6 +220,17 @@ function parseArgs(argv) {
|
|
|
202
220
|
}
|
|
203
221
|
}
|
|
204
222
|
|
|
223
|
+
// A version belongs to ONE package, and only the library uniform judges one: an
|
|
224
|
+
// option no row of the run reads would declare nothing (`automation-gates.md` §1).
|
|
225
|
+
if (parsed.asVersion !== null && !parsed.library && !parsed.envReads) {
|
|
226
|
+
throw new Error('[oa-validate] --as-version needs --library - only the library uniform judges the version '
|
|
227
|
+
+ 'being published. Fix: oa-validate --library <packageDir> --as-version <X.Y.Z>');
|
|
228
|
+
}
|
|
229
|
+
if (parsed.asVersion !== null && parsed.all) {
|
|
230
|
+
throw new Error('[oa-validate] --as-version judges ONE package - --all measures every package, and each has '
|
|
231
|
+
+ 'its own version. Fix: oa-validate --library <packageDir> --as-version <X.Y.Z>');
|
|
232
|
+
}
|
|
233
|
+
|
|
205
234
|
if (parsed.all && !parsed.library) {
|
|
206
235
|
throw new Error('[oa-validate] Option --all needs --library - a service is checked one repository at a time. '
|
|
207
236
|
+ 'Fix: oa-validate --library --all');
|
|
@@ -247,11 +276,12 @@ function packageRelativeRowIds(manifest) {
|
|
|
247
276
|
* against this package alone, and a `workspace` row's answer is filtered down to
|
|
248
277
|
* what lies under this package's root. Nothing is filtered here.
|
|
249
278
|
*
|
|
250
|
-
* @param {{ manifest: object, packageRoot: string, workspaceRoot: string|null
|
|
279
|
+
* @param {{ manifest: object, packageRoot: string, workspaceRoot: string|null,
|
|
280
|
+
* asVersion: string|null }} params — `asVersion` is `--as-version`, null for an audit
|
|
251
281
|
* @returns {{ findings: Array<object>, notRun: Array<object> }}
|
|
252
282
|
*/
|
|
253
|
-
function checkOnePackage({ manifest, packageRoot, workspaceRoot }) {
|
|
254
|
-
const result = runManifest({ manifest, serviceRoot: packageRoot, workspaceRoot });
|
|
283
|
+
function checkOnePackage({ manifest, packageRoot, workspaceRoot, asVersion }) {
|
|
284
|
+
const result = runManifest({ manifest, serviceRoot: packageRoot, workspaceRoot, asVersion });
|
|
255
285
|
const category = declaredCategory(readPackage(packageRoot).json) || NO_CATEGORY;
|
|
256
286
|
|
|
257
287
|
return {
|
|
@@ -299,7 +329,7 @@ function checkEveryPackage({ manifest, workspaceRoot }) {
|
|
|
299
329
|
};
|
|
300
330
|
|
|
301
331
|
for (const bearer of bearers) {
|
|
302
|
-
const one = checkOnePackage({ manifest, packageRoot: bearer.dir, workspaceRoot });
|
|
332
|
+
const one = checkOnePackage({ manifest, packageRoot: bearer.dir, workspaceRoot, asVersion: null });
|
|
303
333
|
for (const finding of one.findings) {
|
|
304
334
|
const where = packageRelative.has(finding.id)
|
|
305
335
|
? `${bearer.relativeDir}/${finding.where}`
|
|
@@ -424,7 +454,11 @@ function runLibrary(options) {
|
|
|
424
454
|
const packageRoot = canonicalRoot(given);
|
|
425
455
|
|
|
426
456
|
const workspaceRoot = resolveWorkspaceRoot({ explicit: options.workspace });
|
|
427
|
-
|
|
457
|
+
// A package outside the checkout this run speaks for is judged against the
|
|
458
|
+
// wrong tree; with no workspace there is no such checkout, and the run says
|
|
459
|
+
// NOT RESOLVED instead (d.940, `workspaceRoot.js` § requirePackageInApiCheckout).
|
|
460
|
+
if (workspaceRoot !== null) requirePackageInApiCheckout(workspaceRoot, packageRoot);
|
|
461
|
+
const run = checkOnePackage({ manifest, packageRoot, workspaceRoot, asVersion: options.asVersion });
|
|
428
462
|
const report = {
|
|
429
463
|
uniform: manifest.uniform,
|
|
430
464
|
blockingSeverities: manifest.blocking_severities,
|
package/src/helpers/README.md
CHANGED
|
@@ -52,7 +52,7 @@ createServiceReadinessTests(__dirname, {
|
|
|
52
52
|
|
|
53
53
|
`logger` defaults to `console` on purpose — in a bootstrap suite stdout is the
|
|
54
54
|
report the developer reads. See
|
|
55
|
-
|
|
55
|
+
`api/docs/standards/FALLBACKS_INVENTORY.md` §5.6.
|
|
56
56
|
|
|
57
57
|
**Score Breakdown** (owned by `ServiceReadinessValidator`):
|
|
58
58
|
- operations: 80 points (required)
|
|
@@ -104,7 +104,7 @@ If validation fails, clear error messages are shown:
|
|
|
104
104
|
✗ Service configuration missing: config/service/config.json
|
|
105
105
|
Type: MISSING_CONFIG
|
|
106
106
|
File: config/service/config.json
|
|
107
|
-
Fix: Create config.json with service metadata. See: /docs/biz/60-templates/service-template.md
|
|
107
|
+
Fix: Create config.json with service metadata. See: api/docs/biz/60-templates/service-template.md
|
|
108
108
|
|
|
109
109
|
⚠️ WARNINGS (1):
|
|
110
110
|
|
|
@@ -193,7 +193,7 @@ module.exports = { createMyTests };
|
|
|
193
193
|
|
|
194
194
|
## Related Documentation
|
|
195
195
|
|
|
196
|
-
-
|
|
197
|
-
-
|
|
198
|
-
- [
|
|
199
|
-
- [
|
|
196
|
+
- `api/docs/standards/TESTING.md` - Testing standards
|
|
197
|
+
- `api/tests/TESTING.md` - SPOT principles
|
|
198
|
+
- [the package README](../../README.md) - Package documentation
|
|
199
|
+
- [DESIGN.md](../../docs/DESIGN.md) - Design principles
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* createServiceReadinessTests(__dirname); // Pass tests/bootstrap directory
|
|
20
20
|
*
|
|
21
21
|
* Related:
|
|
22
|
-
* - /docs/biz/80-decisions/0005-no-http-in-biz-containers.md
|
|
23
|
-
* - /docs/biz/00-model/service-shape.md (zero-HTTP canonical shape)
|
|
24
|
-
* - /shared/connector/conn-orch-validator/README.md - Package documentation
|
|
22
|
+
* - api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md
|
|
23
|
+
* - api/docs/biz/00-model/service-shape.md (zero-HTTP canonical shape)
|
|
24
|
+
* - api/shared/connector/conn-orch-validator/README.md - Package documentation
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
'use strict';
|
|
@@ -32,7 +32,9 @@ const ServiceReadinessValidator = require('../ServiceReadinessValidator');
|
|
|
32
32
|
const MockRegistry = require('../mocks/MockRegistry');
|
|
33
33
|
const { ServiceStructureValidator } = require('../validators/ServiceStructureValidator');
|
|
34
34
|
const { MIN_COOKBOOK_FORMAT_VERSION } = require('../utils/cookbookFormat');
|
|
35
|
-
const {
|
|
35
|
+
const { parseHandlerRef, resolveHandlerModule } = require('../utils/handlerRef');
|
|
36
|
+
const { operationsRuleSentences } = require('../utils/operationsRules');
|
|
37
|
+
const { operationsDocumentFindings } = require('../utils/operationsDocumentRules');
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Resolve every handler an `operations.json` declares, the way the runtime does.
|
|
@@ -75,13 +77,61 @@ function resolveDeclaredHandlers(serviceRoot, operationsFlat) {
|
|
|
75
77
|
return resolved;
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The cookbook the readiness check validates: one `task` step per operation.
|
|
82
|
+
*
|
|
83
|
+
* It must be a cookbook `@onlineapps/cookbook-core` `validateCookbook` accepts,
|
|
84
|
+
* because that is what `CookbookTestUtils.validateCookbook` runs first (d.984);
|
|
85
|
+
* otherwise this helper rejects its own output. The format's `step_id` pattern
|
|
86
|
+
* is `^[a-zA-Z0-9_]+$` (api/docs/biz/40-cookbooks/format.md § Step definition),
|
|
87
|
+
* and operation names carry dashes, so every other character becomes `_`.
|
|
88
|
+
* Two operations that land on one `step_id` are refused by name — a step id
|
|
89
|
+
* addresses one step, and silently keeping either would test half the catalogue.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} serviceName - the service the steps dispatch to
|
|
92
|
+
* @param {Object} operationsFlat - the flat `{ name: spec }` map of operations.json
|
|
93
|
+
* @returns {{version: string, steps: Object[]}} the synthesised cookbook
|
|
94
|
+
* @throws {Error} when two operation names map to the same step_id
|
|
95
|
+
*/
|
|
96
|
+
function buildReadinessCookbook(serviceName, operationsFlat) {
|
|
97
|
+
const operationByStepId = new Map();
|
|
98
|
+
const steps = Object.entries(operationsFlat).map(([name, op]) => {
|
|
99
|
+
const stepId = `test_${name.replace(/[^a-zA-Z0-9_]/g, '_')}`;
|
|
100
|
+
if (operationByStepId.has(stepId)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`[createServiceReadinessTests] Operations "${operationByStepId.get(stepId)}" and "${name}" map to `
|
|
103
|
+
+ `the same step_id "${stepId}" - Expected: names that differ after replacing non-[a-zA-Z0-9_] `
|
|
104
|
+
+ 'with "_". Fix: rename one operation in operations.json.'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
operationByStepId.set(stepId, name);
|
|
108
|
+
return {
|
|
109
|
+
step_id: stepId,
|
|
110
|
+
type: 'task',
|
|
111
|
+
service: serviceName,
|
|
112
|
+
operation: name,
|
|
113
|
+
input: generateMockInput(op.input),
|
|
114
|
+
output: {}
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
// Cookbook FORMAT version (api/docs/biz/40-cookbooks/format.md § Required fields).
|
|
120
|
+
version: MIN_COOKBOOK_FORMAT_VERSION,
|
|
121
|
+
steps
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
78
125
|
/**
|
|
79
126
|
* Create service readiness integration test suite
|
|
80
127
|
*
|
|
81
128
|
* @param {string} testsDir - Path to tests/bootstrap directory (use __dirname)
|
|
82
129
|
* @param {Object} [options] - Optional configuration
|
|
83
130
|
* @param {boolean} [options.includeOptionalChecks=true] - Include cookbook & registry checks
|
|
84
|
-
* @param {number} [options.timeout=15000] -
|
|
131
|
+
* @param {number} [options.timeout=15000] - The budget, in milliseconds, that
|
|
132
|
+
* EVERY test this helper registers runs under. A positive whole number;
|
|
133
|
+
* anything else is refused when the option is read. Omit it to take 15000 —
|
|
134
|
+
* the helper's own default, never jest's.
|
|
85
135
|
* @param {Object} [options.logger=console] - Logger handed to
|
|
86
136
|
* ServiceReadinessValidator (info/warn/error/debug). Defaults to `console`,
|
|
87
137
|
* which is the report channel of a jest bootstrap suite — see the comment at
|
|
@@ -102,6 +152,20 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
102
152
|
timeout = 15000
|
|
103
153
|
} = options;
|
|
104
154
|
|
|
155
|
+
// Read where it is declared, refused where it is read (§4 Fail-Fast). A
|
|
156
|
+
// budget that is not a positive whole number of milliseconds cannot be one:
|
|
157
|
+
// jest reads `0` as "no budget at all", which is the opposite of what a
|
|
158
|
+
// caller asking for a budget means, and a string or a fraction would reach
|
|
159
|
+
// three `test()` calls and decide nothing there.
|
|
160
|
+
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
'[createServiceReadinessTests] options.timeout is not a positive whole number of '
|
|
163
|
+
+ `milliseconds - received ${String(timeout)}. Expected: the budget every test this helper `
|
|
164
|
+
+ 'registers runs under, e.g. 15000. Fix: pass a positive integer as options.timeout, or '
|
|
165
|
+
+ 'omit it to take the default 15000.'
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
105
169
|
// Validate service structure FIRST
|
|
106
170
|
console.log('\n🔍 Validating service structure...\n');
|
|
107
171
|
const structureValidator = new ServiceStructureValidator(serviceRoot);
|
|
@@ -132,7 +196,7 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
132
196
|
// The version comes from package.json, never from config.json — the same
|
|
133
197
|
// single source of truth the runtime uses:
|
|
134
198
|
//
|
|
135
|
-
// shared/connector/service-wrapper/src/ConfigLoader.js
|
|
199
|
+
// shared/connector/service-wrapper/src/ConfigLoader.js, `loadServiceConfig()`:
|
|
136
200
|
// config.service.version = this.loadPackageVersion(basePath);
|
|
137
201
|
//
|
|
138
202
|
// That assignment is unconditional and happens BEFORE `${VAR}` placeholders
|
|
@@ -154,20 +218,7 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
154
218
|
|
|
155
219
|
if (includeOptionalChecks) {
|
|
156
220
|
mockRegistry = new MockRegistry();
|
|
157
|
-
testCookbook =
|
|
158
|
-
// Cookbook FORMAT version — the synthesised cookbook must satisfy the
|
|
159
|
-
// same rule the Tier-1 gate enforces (api/docs/biz/40-cookbooks/format.md
|
|
160
|
-
// § Required fields), otherwise this helper rejects its own output.
|
|
161
|
-
version: MIN_COOKBOOK_FORMAT_VERSION,
|
|
162
|
-
steps: Object.entries(operationsFlat).map(([name, op]) => ({
|
|
163
|
-
step_id: `test-${name}`,
|
|
164
|
-
type: 'task',
|
|
165
|
-
service: serviceName,
|
|
166
|
-
operation: name,
|
|
167
|
-
input: generateMockInput(op.input),
|
|
168
|
-
output: {}
|
|
169
|
-
}))
|
|
170
|
-
};
|
|
221
|
+
testCookbook = buildReadinessCookbook(serviceName, operationsFlat);
|
|
171
222
|
}
|
|
172
223
|
|
|
173
224
|
// `console` is the intended logger here, not a stand-in for one that is
|
|
@@ -228,19 +279,22 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
228
279
|
|
|
229
280
|
const operationsFlat = operations.operations || operations;
|
|
230
281
|
expect(typeof operationsFlat).toBe('object');
|
|
231
|
-
expect(Object.keys(operationsFlat).length).toBeGreaterThan(0);
|
|
232
282
|
|
|
233
|
-
|
|
283
|
+
// `schema_version`, and a dispatch table with nothing in it: the two
|
|
284
|
+
// document rules, asked of the one definition that owns them. The
|
|
285
|
+
// `toBeGreaterThan(0)` that used to stand here was the fifth answer to
|
|
286
|
+
// the second of them (d.465b).
|
|
287
|
+
expect(operationsDocumentFindings(operations)).toEqual([]);
|
|
234
288
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
});
|
|
289
|
+
// The rules are the Registry's, asked of the one definition that owns
|
|
290
|
+
// them. Restating them here was the fourth copy, and — like the other
|
|
291
|
+
// three — it did not know `mutates`, `resource_type` or the kebab-case
|
|
292
|
+
// key rule, so this suite went green on declarations the Registry
|
|
293
|
+
// refuses on registration.
|
|
294
|
+
const verdict = operationsRuleSentences(operationsFlat);
|
|
295
|
+
expect(verdict.sentences).toEqual([]);
|
|
296
|
+
expect(verdict.valid).toBe(true);
|
|
297
|
+
}, timeout);
|
|
244
298
|
|
|
245
299
|
test('every declared handler resolves to an exported function', () => {
|
|
246
300
|
// The shape of the ref says nothing about whether the module is in the
|
|
@@ -254,7 +308,7 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
254
308
|
for (const entry of resolved) {
|
|
255
309
|
expect(typeof entry.handler).toBe('function');
|
|
256
310
|
}
|
|
257
|
-
});
|
|
311
|
+
}, timeout);
|
|
258
312
|
});
|
|
259
313
|
}
|
|
260
314
|
|
|
@@ -287,4 +341,4 @@ function generateMockInput(inputSchema) {
|
|
|
287
341
|
return mockData;
|
|
288
342
|
}
|
|
289
343
|
|
|
290
|
-
module.exports = { createServiceReadinessTests, resolveDeclaredHandlers };
|
|
344
|
+
module.exports = { createServiceReadinessTests, resolveDeclaredHandlers, buildReadinessCookbook };
|
package/src/index.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* Production entry point: {@link ValidationOrchestrator} (7-step pre-validation
|
|
8
8
|
* driven by operations.json — OpenAPI path iteration is NOT supported).
|
|
9
9
|
*
|
|
10
|
-
* @see
|
|
11
|
-
* @see
|
|
12
|
-
* @see
|
|
10
|
+
* @see api/docs/biz/30-operations/registration-wire.md §3 (operations.json)
|
|
11
|
+
* @see api/docs/biz/40-cookbooks/test-runner-flow.md (validation probes)
|
|
12
|
+
* @see api/docs/architecture/validator.md (validator architecture)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const CookbookTestUtils = require('./CookbookTestUtils');
|
|
@@ -79,7 +79,6 @@ module.exports = {
|
|
|
79
79
|
// the rule it implements today (containment, require, export is a function),
|
|
80
80
|
// while the stricter declared form stays in `parseHandlerRef`, so adopting it
|
|
81
81
|
// narrows nothing the wrapper accepts.
|
|
82
|
-
get HANDLER_REF_PATTERN() { return require('./utils/handlerRef').HANDLER_REF_PATTERN; },
|
|
83
82
|
get parseHandlerRef() { return require('./utils/handlerRef').parseHandlerRef; },
|
|
84
83
|
get resolveHandlerModule() { return require('./utils/handlerRef').resolveHandlerModule; },
|
|
85
84
|
|
|
@@ -96,5 +95,15 @@ module.exports = {
|
|
|
96
95
|
|
|
97
96
|
get createServiceReadinessTests() { return createServiceReadinessTests; },
|
|
98
97
|
get BizCiGateContract() { return BizCiGateContract; },
|
|
99
|
-
get IntegrationRun() { return require('./utils/integrationRun'); }
|
|
98
|
+
get IntegrationRun() { return require('./utils/integrationRun'); },
|
|
99
|
+
|
|
100
|
+
// The deploy contract (R1-R5, R7-R8) over one biz repository. It is exported
|
|
101
|
+
// for the same reason `resolveMigrationPlan` above is: the one consumer
|
|
102
|
+
// outside this package — `api/scripts/verify-biz-deploy-contract.sh` — reached
|
|
103
|
+
// it as `…/src/utils/deployContract.js`, and a path inside the package is not
|
|
104
|
+
// a contract. With the `exports` map (d.812) that path stops resolving at all,
|
|
105
|
+
// so the capability moves here, where it is named and kept.
|
|
106
|
+
get verifyDeployContract() { return require('./utils/deployContract').verifyDeployContract; },
|
|
107
|
+
get DEPLOY_CONTRACT_REQUIREMENTS() { return require('./utils/deployContract').DEPLOY_CONTRACT_REQUIREMENTS; },
|
|
108
|
+
get DEPLOY_CONTRACT_SCOPE() { return require('./utils/deployContract').DEPLOY_CONTRACT_SCOPE; }
|
|
100
109
|
};
|