@onlineapps/conn-orch-validator 8.1.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +412 -0
  2. package/README.md +83 -9
  3. package/docs/DESIGN.md +21 -7
  4. package/manifests/biz-service.manifest.json +28 -5
  5. package/package.json +3 -3
  6. package/src/CookbookTestRunner.js +84 -16
  7. package/src/ValidationOrchestrator.js +73 -20
  8. package/src/cli/biz-ci-gate.js +28 -14
  9. package/src/cli/oa-sync-template.js +23 -8
  10. package/src/cli/oa-validate.js +7 -1
  11. package/src/index.js +21 -13
  12. package/src/lint/scripts/lintScripts.js +65 -18
  13. package/src/manifest/checks/composeRunnerBlock.js +37 -20
  14. package/src/manifest/checks/discoveryOrphan.js +2 -1
  15. package/src/manifest/checks/docsLintBridge.js +79 -21
  16. package/src/manifest/checks/gitTracked.js +12 -1
  17. package/src/manifest/checks/libraryPackage.js +3 -1
  18. package/src/manifest/checks/libraryWorkspace.js +18 -3
  19. package/src/manifest/checks/readmeRegion.js +9 -1
  20. package/src/manifest/checks/serviceConfig.js +29 -12
  21. package/src/manifest/checks/serviceFiles.js +34 -7
  22. package/src/manifest/checks/serviceIdentityRows.js +3 -1
  23. package/src/manifest/checks/serviceRuntime.js +3 -1
  24. package/src/manifest/discovery.js +25 -7
  25. package/src/manifest/runManifest.js +58 -7
  26. package/src/manifest/workspaceRoot.js +91 -5
  27. package/src/sync/serviceTemplate.js +76 -7
  28. package/src/sync/sharedEnv.js +11 -4
  29. package/src/sync/uniformFiles.js +91 -21
  30. package/src/utils/bizCiGateContract.js +25 -1
  31. package/src/utils/installContract.js +46 -5
  32. package/src/utils/libCompat.js +39 -19
  33. package/src/utils/preValidation.js +56 -11
  34. package/src/utils/stepFailure.js +106 -19
  35. package/src/utils/testCoverageContract.js +60 -2
  36. package/src/utils/throwawaySchema.js +92 -7
  37. package/src/validatorIdentity.js +31 -0
  38. package/src/validators/ServiceStructureValidator.js +41 -15
  39. package/src/validators/ValidationProofGenerator.js +73 -34
  40. package/templates/business-service/.dockerignore +9 -1
  41. package/templates/business-service/.gitlab-ci.yml +97 -30
  42. package/templates/business-service/README.md +14 -5
  43. package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +17 -5
  44. package/templates/business-service/config/env-templates/shared.env +7 -1
  45. package/templates/business-service/docs/80-setup/INSTALL.md +13 -6
  46. package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +1 -1
  47. package/templates/business-service/docs/80-setup/VALIDATION.md +1 -1
  48. package/templates/business-service/jest.config.js +9 -1
  49. package/templates/business-service/package.json.template +1 -1
  50. package/src/mocks/MockStorage.js +0 -188
@@ -1,7 +1,12 @@
1
1
  # --- oa-dockerignore v1
2
2
  # Derived from templates/business-service/gitignore, the declaration .gitignore reads too —
3
3
  # what a LOCAL production build must not copy into the image (being ignored by git
4
- # excludes nothing from COPY . .). Everything above this block is this repository's own.
4
+ # excludes nothing from COPY . .), PLUS the test tree: tests stay in the repository
5
+ # and out of the artefact that runs, the same decision the library uniform makes as
6
+ # L-PACK-TESTS. The one exception is tests/cookbooks, which is not a test but a
7
+ # declaration the runtime reads — Tier-1 of the boot runs those cookbooks at phase
8
+ # 0.2, and an image without them refuses its own validation proof as NO_TESTS.
9
+ # Everything above this block is this repository's own.
5
10
  node_modules
6
11
  **/node_modules
7
12
  config/runtime
@@ -38,5 +43,8 @@ Thumbs.db
38
43
  **/Thumbs.db
39
44
  .oa_drive_deps_hash
40
45
  **/.oa_drive_deps_hash
46
+ tests
47
+ **/tests
48
+ !tests/cookbooks
41
49
  .git
42
50
  # --- end oa-dockerignore v1
@@ -2,18 +2,28 @@
2
2
  # Written by @onlineapps/conn-orch-validator (row G-CI of
3
3
  # manifests/biz-service.manifest.json). The PLATFORM half of this pipeline: which
4
4
  # pipelines run at all, how a service is built, how its image is identified (R5),
5
- # how the digest reaches the deploy (R1/R2), that the installation document and
6
- # the migrations still agree, the uniform gate that runs before the SSH step
7
- # (confirmation biz-service-manifest 008) and the post-deploy gate that runs
8
- # after it (confirmation deploy-gate-targets 003). npx oa-sync-template .gitlab-ci.yml
9
- # --target . rewrites everything between these two markers and nothing outside
10
- # them.
5
+ # how the digest reaches the deploy (R1/R2), the uniform gate that runs on every
6
+ # pipeline (job validate-uniform, confirmation biz-service-manifest 010) and
7
+ # again before the SSH step as the binding instance (008), and the post-deploy
8
+ # gate that runs after it (confirmation deploy-gate-targets 003). The
9
+ # installation contract is checked by that uniform gate, in the manifest rows
10
+ # G-SETUP, D-DB-PACKAGE and D-DB-HEADERS, and no longer by a job of its own
11
+ # (d.470). npx oa-sync-template .gitlab-ci.yml --target . rewrites everything
12
+ # between these two markers and nothing outside them.
11
13
  #
12
14
  # Outside them is this repository's own: its test job - which database, which
13
15
  # ci:gate:* steps and which artefacts its integration needs is a fact about the
14
16
  # service - and whatever else it runs. The stages this block declares (test,
15
17
  # build, secret-detection, deploy) cover those jobs too; measured over the eight
16
18
  # biz repositories on 2026-09-11, none declares a fifth stage.
19
+ #
20
+ # One thing the platform can only RECOMMEND about that half, because it does not
21
+ # own it: give the service's own jobs the `devel` branch as well. The post-commit
22
+ # mirror pushes every commit there, and a `rules:` list naming only main,
23
+ # production and merge requests is why that branch produced no pipeline at all
24
+ # (010 point 2). The block does its side of it - validate-uniform and
25
+ # secret_detection run on devel; whether a service's own test job should too is
26
+ # the service's call.
17
27
  include:
18
28
  - template: Jobs/Secret-Detection.gitlab-ci.yml
19
29
 
@@ -32,14 +42,71 @@ variables:
32
42
  IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
33
43
  IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
34
44
 
35
- verify-installation-contract:
45
+ # The layout and the runtime every uniform run needs, declared ONCE for the two
46
+ # jobs that run it: the continuous check below and the binding one in
47
+ # deploy-production (confirmations biz-service-manifest 008 and 010). A hidden
48
+ # key is a declaration and never a job - GitLab runs nothing whose name starts
49
+ # with a dot - so this adds no gate, it gives the two that exist one source.
50
+ .oa-uniform:
51
+ # The uniform runs in these jobs, so they need the runtime the pinned engine
52
+ # declares - the same major the test stage and the Dockerfile carry, which is
53
+ # what R3 of the deploy contract compares.
54
+ image: node:24-alpine
55
+ variables:
56
+ # The engine answers the rows that read the platform SSOT only for a service
57
+ # lying at <root>/api_biz/<service> beside <root>/api. GitLab decides where a
58
+ # project is checked out, so the job ASKS for the one place that IS that
59
+ # layout - no symlink (api rule: No Symlinks) and no second copy of this
60
+ # repository, which would leave two trees and no way to tell which one the
61
+ # verdict was about. Needs [runners.custom_build_dir] enabled on the runner.
62
+ #
63
+ # $CI_CONCURRENT_ID is the runner's slot number, and it is what keeps two
64
+ # pipelines of this project off each other's tree. Since 010 the uniform runs
65
+ # on EVERY pipeline, so two of them on one runner is the ordinary case: with
66
+ # one shared path the second run either refuses on an api clone it did not
67
+ # make, or measures the one the first run is still replacing. The slot sits
68
+ # ABOVE api_biz, so the sibling api/ still lands beside this checkout, which
69
+ # is the layout verify-deploy-uniform.sh checks.
70
+ GIT_CLONE_PATH: $CI_BUILDS_DIR/oa-uniform/$CI_CONCURRENT_ID/api_biz/__SERVICE_NAME__
71
+ # Where the platform SSOT is read from, and which state of it a run is
72
+ # measured against. Read access is granted by the api project's CI job-token
73
+ # allowlist - the owner's step, outside any repository (confirmation
74
+ # biz-service-manifest 008 point 2).
75
+ API_PROJECT_PATH: onlineapps/oadrive/infra-mono
76
+ API_UNIFORM_REF: production
77
+
78
+ # The uniform of THIS commit, on every pipeline - confirmation
79
+ # biz-service-manifest 010. Entry 008 places the BINDING run before the SSH step
80
+ # of a production deploy; it never said that is the only place it runs, and read
81
+ # that way it left a service unmeasured between two deploys. Measured on
82
+ # biz-converter 2026-09-16: no job ran the uniform outside the deploy job, the
83
+ # post-commit mirror pushed every commit to origin/devel and pipelines?ref=devel
84
+ # answered 0, and the last pipeline on main was 13 days old. So the same gate,
85
+ # the same bytes and the same arguments run here too, on a merge request, on
86
+ # main, on the devel mirror and on production - and the deploy job keeps its own
87
+ # run as the last, binding instance.
88
+ validate-uniform:
36
89
  stage: test
37
- image: alpine:latest
90
+ extends: .oa-uniform
91
+ before_script:
92
+ # git: the gate clones api read-only beside this checkout. npm ci: the gate
93
+ # and the engine it runs are BOTH the ones this repository pinned, so the
94
+ # version measured is the version this service proved - never a floating
95
+ # download.
96
+ - apk add --no-cache git
97
+ - npm ci
38
98
  script:
39
- - sh scripts/verify-installation-docs-sql-contract.sh
99
+ # The same invocation deploy-production makes. The script is not this
100
+ # repository's own file: no biz repository carries it (measured over all
101
+ # eight, 2026-09-16), and a copy generated into eight of them would be a
102
+ # second place to keep in step (change-discipline.md - One rail per
103
+ # concern). It comes from the package this repository pins, beside the
104
+ # engine it runs.
105
+ - sh node_modules/@onlineapps/conn-orch-validator/templates/business-service/scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$API_UNIFORM_REF"
40
106
  rules:
41
107
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
42
108
  - if: $CI_COMMIT_BRANCH == "main"
109
+ - if: $CI_COMMIT_BRANCH == "devel"
43
110
  - if: $CI_COMMIT_BRANCH == "production"
44
111
 
45
112
  build:
@@ -81,28 +148,18 @@ secret_detection:
81
148
  rules:
82
149
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
83
150
  - if: $CI_COMMIT_BRANCH == "main"
151
+ # The mirror branch too (010 point 2): a secret pushed there is pushed.
152
+ - if: $CI_COMMIT_BRANCH == "devel"
84
153
  - if: $CI_COMMIT_BRANCH == "production"
85
154
 
86
155
  deploy-production:
87
156
  stage: deploy
88
- # The uniform runs here (confirmation biz-service-manifest 008), so the job
89
- # needs the runtime the pinned engine declares — the same major the test stage
90
- # and the Dockerfile carry, which is what R3 of the deploy contract compares.
91
- image: node:24-alpine
157
+ # The BINDING uniform run happens here, before the SSH step (confirmation
158
+ # biz-service-manifest 008). The runtime it needs and the layout it is
159
+ # measured in are the shared declaration above - the same one validate-uniform
160
+ # reads, so the two runs cannot be measuring different things.
161
+ extends: .oa-uniform
92
162
  variables:
93
- # The engine answers the rows that read the platform SSOT only for a service
94
- # lying at <root>/api_biz/<service> beside <root>/api. GitLab decides where a
95
- # project is checked out, so the job ASKS for the one place that IS that
96
- # layout — no symlink (api rule: No Symlinks) and no second copy of this
97
- # repository, which would leave two trees and no way to tell which one the
98
- # verdict was about. Needs [runners.custom_build_dir] enabled on the runner.
99
- GIT_CLONE_PATH: $CI_BUILDS_DIR/oa-uniform/api_biz/__SERVICE_NAME__
100
- # Where the platform SSOT is read from, and which state of it a production
101
- # deploy is measured against. Read access is granted by the api project's CI
102
- # job-token allowlist — the owner's step, outside any repository
103
- # (confirmation biz-service-manifest 008 point 2).
104
- API_PROJECT_PATH: onlineapps/oadrive/infra-mono
105
- API_UNIFORM_REF: production
106
163
  # Which checks judge this deploy. The gate has no default target: the job
107
164
  # declares what it is measured by (confirmation deploy-gate-targets 002),
108
165
  # and the name is the service's registry identity — the one
@@ -190,7 +247,7 @@ deploy-production:
190
247
  # not look everywhere, and nothing reaches the box. There is no flag and no
191
248
  # variable that turns this off (automation-gates.md §1 requirement 5).
192
249
  - npm ci
193
- - scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$API_UNIFORM_REF"
250
+ - sh node_modules/@onlineapps/conn-orch-validator/templates/business-service/scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$API_UNIFORM_REF"
194
251
  # R1: refuse to deploy without the immutable target the build stage published.
195
252
  - |
196
253
  if [ -z "${BIZ_IMAGE_DIGEST:-}" ]; then
@@ -357,9 +414,20 @@ deploy-production:
357
414
  # format the runbook writes in phase 1, so a schema already carried
358
415
  # forward is skipped rather than applied twice (confirmation 001:
359
416
  # "fáze 2 nesmí změnit sémantiku trackeru").
360
- mkdir -p config/runtime
417
+ #
418
+ # Its EXISTENCE is the precondition, and the step never creates it.
419
+ # Confirmation 001 splits the work in two: phase 1 - schema, account and
420
+ # the first run of this same runner - is an operator's step in the
421
+ # runbook, and phase 2 is this job carrying that state forward. On a box
422
+ # phase 1 never touched there is no tracker, and a job that created one
423
+ # would apply the WHOLE series unattended on the first deploy, which is
424
+ # precisely the phase the owner kept manual. So a missing tracker stops
425
+ # the deploy and names the runbook, exactly as a missing schema does.
361
426
  TRACKER="config/runtime/applied-migrations-${DB_SCHEMA}.txt"
362
- touch "$TRACKER"
427
+ if [ ! -f "$TRACKER" ]; then
428
+ echo "[deploy] Migrations tracker $DEPLOY_PATH/$TRACKER missing - phase 1 (the migrations of $DB_SCHEMA applied by hand with the same runner) has not run on this box. Expected: the flat tracker phase 1 leaves behind, carried forward by this deploy. Fix: run the business-schema migrations step of api/docs/setup/INSTALL.md for $DB_SCHEMA, then re-run this deploy. This step never creates the tracker: a deploy that did would apply the whole series unattended on a box nobody had provisioned." >&2
429
+ exit 1
430
+ fi
363
431
  apply_mariadb_migrations_over_tcp "$DB_MIGRATIONS_DIR" "$TRACKER" "$DB_HOST" "$DB_PORT" "$DB_SCHEMA"
364
432
  # The runner logs each file it applies; what this line adds is the state
365
433
  # that outlives the deploy. The runner's own counter of files it passed
@@ -398,7 +466,6 @@ test:
398
466
  image: node:24-alpine
399
467
  variables:
400
468
  RABBITMQ_URL: "amqp://localhost:5672"
401
- REGISTRY_URL: "http://localhost:33100"
402
469
  NODE_ENV: "test"
403
470
  script:
404
471
  - npm ci
@@ -6,7 +6,7 @@ Every `api_biz/*/package.json` that `api/config/services.json` lists wears it (`
6
6
  Duty sections that apply:
7
7
 
8
8
  - `files`: F-INIT, F-JEST, F-RUNNER, F-GITIGNORE, F-DOCKERIGNORE, G-PROD, G-CI, G-PROD-IMAGE, G-README, G-SETUP, G-SETUP-INSTALL, G-SETUP-MATRIX, G-SETUP-VALIDATION, X-IGNORED, X-HOOKS, X-PREVAL, X-DB-CONFIG
9
- - `scripts`: S-TEST, S-ALL-C, S-INT-C, S-COOKBOOKS, S-HOST, S-HOOKS
9
+ - `scripts`: S-TEST, S-ALL-C, S-INT-C, S-UNIT-C, S-COOKBOOKS, S-COOK-C, S-HOST, S-HOOKS
10
10
  - `tooling`: S-SCRIPTS
11
11
  - `config`: C-IDENTITY, C-SERVICE, C-OPS, C-CONTRACT, C-CONNECTORS, C-SERVICE-DEAD, C-ENV, G-SHARED-ENV, C-ENV-READS
12
12
  - `runtime`: R-NODE, R-MEM, R-PID1, R-PORTS-DEV, R-PORTS
@@ -21,7 +21,7 @@ Paths a duty owns:
21
21
  - `README.md`: G-README (from `./node_modules/@onlineapps/conn-orch-validator/manifests/biz-service.manifest.json`)
22
22
  - `config/biz-docs-lint.tree.json`: C-LINT
23
23
  - `config/env-templates`: C-ENV, D-DB-ACCOUNT
24
- - `config/env-templates/shared.env`: G-SHARED-ENV (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/config/env-templates/shared.env`)
24
+ - `config/env-templates/shared.env`: G-SHARED-ENV (from `api/config/shared-env.json`)
25
25
  - `config/service/config.json`: C-SERVICE
26
26
  - `config/service/integration-contract.json`: C-CONTRACT, D-DB-COLLATION
27
27
  - `config/service/operations.json`: C-OPS
@@ -125,7 +125,7 @@ own memory budget, so a heavy suite can no longer be killed together with the
125
125
  service, and the `test` profile keeps it out of a plain `docker compose up`.
126
126
 
127
127
  ```bash
128
- npm run test:container # unit suite, in the runner
128
+ npm run test:unit:container # unit suite, in the runner
129
129
  npm run test:all:container # everything test:all chains, in the runner
130
130
  ```
131
131
 
@@ -141,14 +141,23 @@ the `Dockerfile` or to the dependencies:
141
141
  docker compose --profile test build
142
142
  ```
143
143
 
144
- Without it the next `npm run test:container` runs the image the runner was last
144
+ Without it the next `npm run test:unit:container` runs the image the runner was last
145
145
  built from, and a suite then passes or fails on code that is no longer in this
146
146
  repository — a green run that says nothing about what is committed.
147
147
 
148
+ `jest.config.js` is held byte for byte by row `F-JEST`, so a local
149
+ `setupFiles` entry pointing at a `tests/setup-env.js` of your own does not
150
+ survive a sync — and does not need to. What a suite needs from the environment it
151
+ gets from the runner's `env_file`, the same files the service itself loads, which
152
+ is why the runner exists at all; a setup file that put those names into
153
+ `process.env` would be a second, quieter source for them
154
+ (`.claude/rules/change-discipline.md` § One rail per concern). A suite that needs
155
+ a value nothing declares says so by failing on the name, which is the point.
156
+
148
157
  `npm test` and `npm run test:unit` are what runs INSIDE that container; the
149
158
  runner is the one rail a suite runs on (owner decision
150
159
  `api/docs/governance/confirmations/biz-test-container.md` 001, and manifest rows
151
- `S-ALL-C` and `S-HOST`). Running them on the host is not a second, quicker way
160
+ `S-UNIT-C`, `S-ALL-C`, `S-COOK-C` and `S-HOST`). Running them on the host is not a second, quicker way
152
161
  to the same answer: the host has neither the service's `env_file` nor its
153
162
  network, so what fails there says nothing about the service — which is why no
154
163
  `test:host` script exists and why the uniform forbids one.
@@ -1,7 +1,12 @@
1
1
  # __SERVICE_NAME__ environment
2
2
  # Copy to config/env-active/__SERVICE_NAME__.env and adjust values.
3
-
4
- SERVICE_NAME=__SERVICE_NAME__
3
+ #
4
+ # The service's own name is NOT here. config/service/config.json declares it and
5
+ # ServiceWrapper._serviceName() reads it there, passing it on to monitoring and
6
+ # to the MQ client; an env key repeating it would be a second bearer of one fact,
7
+ # and the copies measured on 2026-09-16 had already drifted from it (the template
8
+ # rendered the short name, every live file the registry name). Row C-IDENTITY
9
+ # holds every spelling in this repository to config/service/config.json.
5
10
 
6
11
  # Schema migrations at deploy time. The deploy job (block `oa-ci v1` of
7
12
  # .gitlab-ci.yml) runs api/scripts/lib/mariadb-migrations.sh between `compose
@@ -15,8 +20,15 @@ SERVICE_NAME=__SERVICE_NAME__
15
20
  #
16
21
  # Commented out because this scaffold has no database: its
17
22
  # config/service/integration-contract.json declares no `database` block, the
18
- # deploy step prints NOT APPLICABLE and reads neither name. A service that
19
- # declares that block uncomments both here and fills the values in
20
- # config/env-active/ only - per machine, never in git.
23
+ # deploy step prints NOT APPLICABLE and reads neither name.
24
+ #
25
+ # Where the values live is the host contract, not a choice: the deploy job reads
26
+ # config/env-active/<service>.env ON THE BOX, so a service that declares a
27
+ # `database` block uncomments both names HERE (the template, in git, values
28
+ # empty) and fills them in config/env-active/ on each machine - never in git. The
29
+ # account itself is not created by any of this: on a first deployment the owner
30
+ # of the box creates the schema and the service account by hand from the runbook,
31
+ # and the migration runner then runs under that account (db-migrations-first-deploy
32
+ # 001, phase 1).
21
33
  # MARIADB_MIGRATION_USER=
22
34
  # MARIADB_MIGRATION_PASSWORD=
@@ -4,9 +4,15 @@
4
4
  # The runtime environment name every platform library resolves through its own runtime-config schema; without it a library cannot say which environment it is reporting from.
5
5
  NODE_ENV=development
6
6
 
7
- # The log level an infra service's config/logging.json resolves through ${LOG_LEVEL}; one platform value so a stack does not log at four different levels.
7
+ # The log level an infrastructure service's config/logging.json resolves through ${LOG_LEVEL}; one platform value so a stack does not log at a different level in every service. In the business chain the key is carried, not read: no library in the business chain reads it - @onlineapps/monitoring-core reads no environment variable at all (principle 1) and takes its level from the config the service hands it - so setting it on a business service changes nothing. It reaches every bearer because the shared key set is ONE file whose every copy is byte-identical to the platform template (confirmation biz-service-manifest 003 §18), never because each bearer reads every key.
8
8
  LOG_LEVEL=info
9
9
 
10
+ # How large one log file may grow before @onlineapps/monitoring-core rotates it (src/logger.js REQUIRED_FILE_BOUNDS, src/config.js logMaxSize). From monitoring-core 3.0.0 the bound has no default in code, so a service that declares it nowhere fails at logger construction by design, and the value has to be declared - it is shared because the ceiling is a property of the box every service writes onto, not a preference of one service: the age bound LOG_MAX_DAYS alone let a single day's file fill the disk (hello wrote 2.4 GB/day before d.383). The platform value is 50 MB, the same in dev and production (owner decision docs/governance/confirmations/log-file-bounds.md 001).
11
+ LOG_MAX_SIZE_BYTES=52428800
12
+
13
+ # How many rotated files of one log row the directory keeps - the other half of the size bound, and equally without a default in @onlineapps/monitoring-core 3.0.0 (src/logger.js REQUIRED_FILE_BOUNDS, src/config.js logMaxFiles). Ten files of LOG_MAX_SIZE_BYTES is the 500 MB per service the platform declares as its ceiling, so the two keys are read together and travel together (owner decision docs/governance/confirmations/log-file-bounds.md 001).
14
+ LOG_MAX_FILES=10
15
+
10
16
  # The HMAC-SHA256 secret the gateway and auth sign and verify access tokens with; it is shared because both ends of one token must hold the same value (docs/standards/JWT_AUTH.md).
11
17
  JWT_SECRET=CHANGE_ME
12
18
 
@@ -99,7 +99,7 @@ if it is missing, so onboarding a new service needs no manual step there.
99
99
  Between that `pull` and that `up` the deploy job applies this service's pending
100
100
  MariaDB migrations — the ordering is the safety: the new image is already on the
101
101
  box, the previous container is still serving, and a migration that fails stops
102
- the deploy before the switch. Two facts decide what happens:
102
+ the deploy before the switch. Three facts decide what happens:
103
103
 
104
104
  - `config/service/integration-contract.json` → `database`. This scaffold declares
105
105
  no such block, so the step prints `NOT APPLICABLE (no database block)` and the
@@ -109,11 +109,18 @@ the deploy before the switch. Two facts decide what happens:
109
109
  `MARIADB_MIGRATION_PASSWORD`, the service's own account. The step creates
110
110
  neither the schema nor the account and refuses the deploy when that account
111
111
  cannot open the schema, naming the runbook instead.
112
-
113
- Who creates them, with which grants and which collation, is the runbook's step
114
- and is not repeated here: `api/docs/setup/INSTALL.md`, the business-schema step.
115
- The decision behind both halves — two phases, the service account as the
116
- isolation guard, one runner and one tracker across them — is owner confirmation
112
+ - `config/runtime/applied-migrations-<schema>.txt` on the box — the tracker.
113
+ **It must already be there**, and this job never creates it. The file is what
114
+ phase 1 leaves behind: the operator who created the schema and the account also
115
+ ran the first pass of the same runner by hand, from the runbook. A deploy onto a
116
+ box phase 1 never touched therefore stops with `Migrations tracker … missing`
117
+ rather than applying the whole series unattended.
118
+
119
+ Who creates the schema, the account and that first tracker — with which grants and
120
+ which collation — is the runbook's step and is not repeated here:
121
+ `api/docs/setup/INSTALL.md`, the business-schema step. The decision behind both
122
+ halves — two phases, the service account as the isolation guard, one runner and
123
+ one tracker across them — is owner confirmation
117
124
  `api/docs/governance/confirmations/db-migrations-first-deploy.md` 001.
118
125
 
119
126
  ## Status
@@ -24,7 +24,7 @@ the service.
24
24
 
25
25
  ```bash
26
26
  docker compose up -d --build
27
- npm run test:container
27
+ npm run test:unit:container
28
28
  npm run test:cookbooks:container
29
29
  ```
30
30
 
@@ -23,7 +23,7 @@ Every command runs from the repository root (`api_biz/__SERVICE_NAME__/`).
23
23
  node node_modules/@onlineapps/conn-orch-validator/src/cli/biz-ci-gate.js verify-contract
24
24
 
25
25
  # 2 — unit suite, in the one-shot runner beside the service
26
- npm run test:container
26
+ npm run test:unit:container
27
27
 
28
28
  # 3 — cookbooks, in the same runner (it has the env files)
29
29
  npm run test:cookbooks:container
@@ -1,6 +1,14 @@
1
1
  module.exports = {
2
2
  testEnvironment: 'node',
3
3
  testMatch: ['**/tests/**/*.test.js'],
4
- testTimeout: 30000,
4
+ // 120 s, because the limit is there to catch a test that HANGS and not a
5
+ // queue for the processor: on the shared dev machine (load average 47-94,
6
+ // the runners of several services at once) five integration suites failed in
7
+ // beforeAll at the previous 30 s while each of them passed when run alone,
8
+ // and the whole run passed at 180 s (BIZ-converter, d.580). The number is the
9
+ // platform's, not a service's - row F-JEST holds this file byte for byte.
10
+ testTimeout: 120000,
11
+ // One suite at a time, which is what the runner's memory norm assumes
12
+ // (api/docs/governance/confirmations/biz-memory-limits.md 001).
5
13
  maxWorkers: 1
6
14
  };
@@ -13,7 +13,7 @@
13
13
  "test:ci": "jest",
14
14
  "test:unit": "jest tests/unit",
15
15
  "test:all": "npm run test:unit",
16
- "test:container": "docker compose run --rm __CONTAINER_NAME___tests npm run test:unit",
16
+ "test:unit:container": "docker compose run --rm __CONTAINER_NAME___tests npm run test:unit",
17
17
  "test:all:container": "docker compose run --rm __CONTAINER_NAME___tests npm run test:all",
18
18
  "test:coverage": "jest --coverage",
19
19
  "test:cookbooks": "node node_modules/@onlineapps/conn-orch-validator/src/cli/biz-ci-gate.js run-prevalidation",
@@ -1,188 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * MockStorage - Simulates MinIO/S3 storage for testing
5
- */
6
- class MockStorage {
7
- constructor() {
8
- this.buckets = {};
9
- this.metadata = {};
10
- }
11
-
12
- /**
13
- * Create bucket
14
- */
15
- async createBucket(bucketName) {
16
- if (!this.buckets[bucketName]) {
17
- this.buckets[bucketName] = {};
18
- }
19
- return { success: true };
20
- }
21
-
22
- /**
23
- * Put object
24
- */
25
- async put(bucket, key, content, options = {}) {
26
- if (!this.buckets[bucket]) {
27
- await this.createBucket(bucket);
28
- }
29
-
30
- const data = typeof content === 'object' ? JSON.stringify(content) : content;
31
-
32
- this.buckets[bucket][key] = {
33
- content: data,
34
- contentType: options.contentType || 'application/json',
35
- metadata: options.metadata || {},
36
- timestamp: Date.now(),
37
- size: data.length
38
- };
39
-
40
- // Store metadata separately for querying
41
- if (!this.metadata[bucket]) {
42
- this.metadata[bucket] = {};
43
- }
44
- this.metadata[bucket][key] = {
45
- size: data.length,
46
- lastModified: Date.now(),
47
- contentType: options.contentType || 'application/json'
48
- };
49
-
50
- return {
51
- success: true,
52
- etag: `mock-etag-${Date.now()}`,
53
- location: `${bucket}/${key}`
54
- };
55
- }
56
-
57
- /**
58
- * Get object
59
- */
60
- async get(bucket, key) {
61
- if (!this.buckets[bucket] || !this.buckets[bucket][key]) {
62
- throw new Error(`[MockStorage] Object not found: ${bucket}/${key} - Expected the object to have `
63
- + 'been stored first. Fix: put(bucket, key, content) in the test setup before reading it.');
64
- }
65
-
66
- const obj = this.buckets[bucket][key];
67
- let content = obj.content;
68
-
69
- // Try to parse JSON if content type suggests it
70
- if (obj.contentType === 'application/json' && typeof content === 'string') {
71
- try {
72
- content = JSON.parse(content);
73
- } catch (e) {
74
- // Keep as string if parsing fails
75
- }
76
- }
77
-
78
- return {
79
- content,
80
- metadata: obj.metadata,
81
- contentType: obj.contentType,
82
- size: obj.size,
83
- lastModified: obj.timestamp
84
- };
85
- }
86
-
87
- /**
88
- * Delete object
89
- */
90
- async delete(bucket, key) {
91
- if (this.buckets[bucket]) {
92
- delete this.buckets[bucket][key];
93
- if (this.metadata[bucket]) {
94
- delete this.metadata[bucket][key];
95
- }
96
- }
97
- return { success: true };
98
- }
99
-
100
- /**
101
- * List objects in bucket
102
- */
103
- async list(bucket, prefix = '', limit = 1000) {
104
- if (!this.buckets[bucket]) {
105
- return { objects: [] };
106
- }
107
-
108
- const objects = Object.keys(this.buckets[bucket])
109
- .filter(key => key.startsWith(prefix))
110
- .slice(0, limit)
111
- .map(key => ({
112
- key,
113
- size: this.buckets[bucket][key].size,
114
- lastModified: this.buckets[bucket][key].timestamp
115
- }));
116
-
117
- return { objects };
118
- }
119
-
120
- /**
121
- * Check if object exists
122
- */
123
- async exists(bucket, key) {
124
- return !!(this.buckets[bucket] && this.buckets[bucket][key]);
125
- }
126
-
127
- /**
128
- * Get object metadata
129
- */
130
- async getMetadata(bucket, key) {
131
- if (!this.metadata[bucket] || !this.metadata[bucket][key]) {
132
- throw new Error(`[MockStorage] Metadata not found: ${bucket}/${key} - Expected metadata written `
133
- + 'by put(). Fix: store the object with put(bucket, key, content) before reading its metadata.');
134
- }
135
- return this.metadata[bucket][key];
136
- }
137
-
138
- /**
139
- * Generate presigned URL (mock)
140
- */
141
- async getPresignedUrl(bucket, key, expiresIn = 3600) {
142
- return {
143
- url: `http://mock-storage/${bucket}/${key}?token=mock-token-${Date.now()}`,
144
- expiresAt: Date.now() + (expiresIn * 1000)
145
- };
146
- }
147
-
148
- /**
149
- * Clear all data
150
- */
151
- clear() {
152
- this.buckets = {};
153
- this.metadata = {};
154
- }
155
-
156
- /**
157
- * Get storage stats
158
- */
159
- getStats() {
160
- const stats = {
161
- bucketCount: Object.keys(this.buckets).length,
162
- totalObjects: 0,
163
- totalSize: 0
164
- };
165
-
166
- Object.values(this.buckets).forEach(bucket => {
167
- Object.values(bucket).forEach(obj => {
168
- stats.totalObjects++;
169
- stats.totalSize += obj.size;
170
- });
171
- });
172
-
173
- return stats;
174
- }
175
-
176
- /**
177
- * Copy object
178
- */
179
- async copy(sourceBucket, sourceKey, destBucket, destKey) {
180
- const source = await this.get(sourceBucket, sourceKey);
181
- return await this.put(destBucket, destKey, source.content, {
182
- contentType: source.contentType,
183
- metadata: source.metadata
184
- });
185
- }
186
- }
187
-
188
- module.exports = MockStorage;