|
| 1 | +--- |
| 2 | +'@objectstack/spec': minor |
| 3 | +'@objectstack/core': minor |
| 4 | +'@objectstack/cli': minor |
| 5 | +--- |
| 6 | + |
| 7 | +feat(spec,core,cli)!: a scenario's `requires` is checked before it runs — unmet `params` or `services` SKIP it with a reason; `requires.plugins` is retired into `requires.services` (#20289) |
| 8 | + |
| 9 | +Clause-②: yes (narrowing) |
| 10 | + |
| 11 | +**BREAKING** — shipped as `minor` under the launch-window convention |
| 12 | +(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by |
| 13 | +this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, |
| 14 | +never by the level). |
| 15 | + |
| 16 | +A Quality Protocol scenario's `requires` block declared preconditions — |
| 17 | +`params` (environment variables) and `plugins` (plugins that must be loaded) — |
| 18 | +that nothing checked: measured on a stub target, a scenario naming a missing |
| 19 | +plugin and an unset variable reported PASSED exactly like its no-requirements |
| 20 | +control. ADR-0049 enforce-or-remove, verdict ENFORCE (the mainstream has |
| 21 | +declared preconditions: JUnit `@EnabledIfEnvironmentVariable`, pytest `skipif`), |
| 22 | +ruled B for the shape: each key is judged against something `os test` can |
| 23 | +actually observe. |
| 24 | + |
| 25 | +- **`requires.params`** — each variable must be set to a non-empty value in the |
| 26 | + environment of the process running `os test` (not the target server's, which a |
| 27 | + suite cannot see). An empty value counts as unset: an unconfigured CI secret |
| 28 | + arrives as an empty string. |
| 29 | +- **`requires.services`** (new) — each entry is a discovery service key |
| 30 | + (`CoreServiceName`: `auth`, `automation`, `analytics`, `ai`, `storage`, …; a |
| 31 | + misspelling is refused when the suite loads) that the target must declare |
| 32 | + `enabled` with status `available` in its discovery document (ADR-0076 D12). |
| 33 | + It is read from the discovery request the HTTP adapter already makes once per |
| 34 | + run; a suite that requires no service issues no extra request. |
| 35 | +- **SKIPPED.** A scenario with an unmet entry runs no step — `setup` included — |
| 36 | + and `os test` prints it with its reason, naming every unmet entry and, for a |
| 37 | + service, the services the target does declare available: |
| 38 | + `Skipped: requires.services 'ai' is not available on the target (enabled: false, status: unavailable). The target declares available: auth, data, metadata.` |
| 39 | + It is counted on its own — `SUCCESS: 3 scenarios passed. 1 skipped (not run, not counted as passed).` — |
| 40 | + and never as passed. Skips alone exit `0`; a run in which EVERY selected |
| 41 | + scenario was skipped prints `No scenario ran: …` instead of `SUCCESS`, exits |
| 42 | + `0`, and exits `1` under `--fail-on-empty`. With nothing skipped, the summary |
| 43 | + lines keep their spelling. |
| 44 | +- **`@objectstack/core`:** `QA.TestResult` gains `status` (`'passed' | 'failed' | 'skipped'`) |
| 45 | + and, on a skipped result, `skipped` (`reason`, `unmet[]`, `availableServices`); |
| 46 | + `passed` stays and is `false` on a skip. `TestRunner` takes an optional |
| 47 | + `{ env }` (default: this process's environment), and `TestExecutionAdapter` |
| 48 | + gains an optional `readTargetServices()` — `HttpTestAdapter` answers it from |
| 49 | + its one discovery probe. An adapter without it skips a service requirement |
| 50 | + rather than running it. |
| 51 | + |
| 52 | +``` |
| 53 | +FROM { "id": "ai-summary", "requires": { "plugins": ["@objectstack/service-ai"] }, "steps": [...] } |
| 54 | + -> ran anyway; the missing plugin surfaced as whatever failure it caused, or passed |
| 55 | +TO -> os test refuses the suite at load: |
| 56 | + ✗ scenarios.0.requires.plugins: `scenarios[].requires.plugins` was removed in |
| 57 | + @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever checked it: … |
| 58 | + Delete the key and name the service the scenario needs in `requires.services`, … |
| 59 | + Plugin → service: @objectstack/service-analytics → analytics, @objectstack/plugin-auth → auth, … |
| 60 | +
|
| 61 | +FROM { "requires": { "services": ["ai"] }, … } (new key) |
| 62 | +TO -> against a target whose discovery does not declare `ai` enabled and available: |
| 63 | + ⏭️ Scenario: Summarise an account [ai-summary] (skipped) |
| 64 | + Skipped: requires.services 'ai' is not available on the target (…). The target declares available: … |
| 65 | +``` |
| 66 | + |
| 67 | +**Fix.** `requires.plugins: ["<package>"]` → `requires.services: ["<service>"]`, |
| 68 | +using the mapping the refusal prints (derived from `CORE_SERVICE_PROVIDER`, the |
| 69 | +provider table discovery itself reports): `@objectstack/plugin-auth` → `auth`, |
| 70 | +`@objectstack/service-analytics` → `analytics`, `@objectstack/service-automation` |
| 71 | +→ `automation`, `@objectstack/service-storage` → `storage`, and so on; the `ai` |
| 72 | +service is provided by ObjectStack Cloud/Enterprise. A plugin that fills no |
| 73 | +discovery service slot has no service to require — gate that scenario with a |
| 74 | +`params` variable or select it with `--tags`. `tsc` refuses `plugins` at a typed |
| 75 | +authoring site (its input type is `never`). A `TestResult` consumer that counted |
| 76 | +`!passed` as a failure should read `status` — a skipped result is `passed: false` |
| 77 | +and is not a failure. |
| 78 | + |
| 79 | +**What does not change.** A scenario without `requires` runs exactly as before, |
| 80 | +and a suite that requires no service issues no discovery request it did not |
| 81 | +already issue. |
| 82 | + |
| 83 | +### The retirement kit |
| 84 | + |
| 85 | +- **Schema.** `TestScenarioSchema.requires` is a non-strict `z.object()`, so |
| 86 | + `plugins` is a `retiredKey()` tombstone carrying its prescription (a bare |
| 87 | + deletion would have stripped it in silence); `services` is new, closed over |
| 88 | + `CoreServiceName`. |
| 89 | +- **ADR-0087.** `RETIRED_KEYS_BY_MAJOR[18]` gains `qa/TestScenario:requires.plugins`. |
| 90 | + No D2 conversion: a QA suite is a loose JSON file `os test` loads, never a |
| 91 | + stack collection member or a stored row. The family's D3 entry, |
| 92 | + `qa-scenario-requires-plugins-retired`, carries the prescription to |
| 93 | + `os migrate meta` and the upgrade guide. |
| 94 | +- **Ledger and docs.** `liveness/qa.json` moves `qa.scenarios.requires` from |
| 95 | + `dead` to `live`, citing the runner's judgement and the adapter as producer; |
| 96 | + `state-counts.md` moves `qa` to 9 live / 0 dead. The `os test` section of the |
| 97 | + CLI reference documents the check, the skip line and the exit posture, and the |
| 98 | + generated `qa/testing` reference page is regenerated. |
| 99 | + |
| 100 | +<!-- adr-0087: registered qa-scenario-requires-plugins-retired --> |
0 commit comments