@openwop/openwop-conformance 2.0.0-rc.19 → 2.0.0-rc.28

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/README.md CHANGED
@@ -112,7 +112,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
112
112
 
113
113
  ## What's Covered
114
114
 
115
- The current suite has 499 scenario files under `src/scenarios/`.
115
+ The current suite has 500 scenario files under `src/scenarios/`.
116
116
  - 2026-09-03 (suite `1.157.0 -> 1.158.0`, gap G17): NEW `idempotency-concurrent-claim.test.ts` — drives the new `host-sample-test-seams.md` §25 concurrent duplicate-delivery seam for the RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" atomic-claim MUST, which is unconditional and had no witness of any kind. Asserts every executor mints the SAME `logicalInvocationId` **before** asserting `delivered === 1` — without the identity check a host passes by minting different ids and never colliding, one effect because nothing raced. Not profile-gated and so not opt-out-able (the obligation is unconditional); an unmounted seam records `blocked`, which is not certifiable. Graduates `layer2-invocation-claim-atomic` reference-impl -> protocol.
117
117
  - 2026-08-19 (suite `1.137.0 → 1.138.0`): NEW `durability-poison-exhaustion.test.ts` — RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts what `failure-path.test.ts` cannot: not just that deterministically failing work reaches terminal, but that attempts STOP — counted on the log, re-counted after a scaled quiet window, asserted unchanged. A host still redelivering records more. Seam-gated on the existing event-log seam (`blocked` = unobservable, not unmet) and outside every profile floor.
118
118
  - 2026-08-19 (suite `1.136.15 → 1.137.0`): NEW `replay-fanout-suppression.test.ts` — capability-gated on `webhooks.supported`, **outside every profile floor**; witnesses `replay.md` §"Host-initiated fan-out is an external effect", which was the largest normative MUST NOT on the replay surface with no scenario and no SECURITY invariant. Three legs in ONE `it` against ONE receiver and ONE subscription — a positive control, the MUST NOT, and a `branch` boundary leg — because "no delivery arrived" passes identically when delivery never worked, so absence is asserted only after presence is proven on that exact wiring. A host with an SSRF guard correctly refuses the loopback receiver and records `blocked`: **unobservable, not unmet.**
@@ -457,7 +457,7 @@ Server-required (added in 1.7.0):
457
457
  | ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
458
458
  | **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
459
459
 
460
- Current source tree: 499 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
460
+ Current source tree: 500 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
461
461
 
462
462
  ## Remaining Gaps
463
463
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.0.0-rc.19",
4
- "stampSha256": "586fb1ee88414176538723785bdca6d0ecc5dee48cbafa8f889b7960e206d9da"
3
+ "version": "2.0.0-rc.28",
4
+ "stampSha256": "c7d6661057eca87dffb69c7d262e9b0dd6025087be2950df2007214fda6b0e57"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.0.0-rc.19",
3
+ "version": "2.0.0-rc.28",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,6 +56,6 @@
56
56
  "@openwop/spec-artifacts": "file:../spec-artifacts"
57
57
  },
58
58
  "peerDependencies": {
59
- "@openwop/spec-artifacts": "2.0.0-rc.19"
59
+ "@openwop/spec-artifacts": "2.0.0-rc.28"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
- "files": 545,
6
- "tests": 2119,
7
- "withStableId": 2119,
5
+ "files": 546,
6
+ "tests": 2122,
7
+ "withStableId": 2122,
8
8
  "interpolatedTitles": 0,
9
9
  "explicitIds": 2057
10
10
  },
@@ -12262,6 +12262,58 @@
12262
12262
  }
12263
12263
  ]
12264
12264
  },
12265
+ {
12266
+ "id": "openwop.it.era-key-stamped-v1.a-run-the-host-just-created-carries-eventlogschemaversion",
12267
+ "file": "era-key-stamped-v1.test.ts",
12268
+ "line": 75,
12269
+ "title": "a run the host just created carries eventLogSchemaVersion",
12270
+ "explicitId": null,
12271
+ "citations": [
12272
+ {
12273
+ "section": null,
12274
+ "requirement": "every persisted run document MUST carry an eventLogSchemaVersion — the field is prose-only (run-snapshot.schema.json requires just runId, workflowId and status), so a snapshot without it validates cleanly and only this check can see its absence",
12275
+ "interpolated": true
12276
+ },
12277
+ {
12278
+ "section": null,
12279
+ "requirement": null,
12280
+ "interpolated": true
12281
+ }
12282
+ ]
12283
+ },
12284
+ {
12285
+ "id": "openwop.it.era-key-stamped-v1.a-freshly-created-run-is-not-classified-legacy-by-the-host-s-own-rule",
12286
+ "file": "era-key-stamped-v1.test.ts",
12287
+ "line": 98,
12288
+ "title": "a freshly created run is not classified legacy by the host's own rule",
12289
+ "explicitId": null,
12290
+ "citations": [
12291
+ {
12292
+ "section": "spec/v1/version-negotiation.md §Legacy detection",
12293
+ "requirement": null,
12294
+ "interpolated": true
12295
+ }
12296
+ ]
12297
+ },
12298
+ {
12299
+ "id": "openwop.it.era-key-stamped-v1.a-run-the-host-just-created-carries-engineversion-the-legacy-escape-cannot-reach",
12300
+ "file": "era-key-stamped-v1.test.ts",
12301
+ "line": 124,
12302
+ "title": "a run the host just created carries engineVersion — the legacy escape cannot reach it",
12303
+ "explicitId": null,
12304
+ "citations": [
12305
+ {
12306
+ "section": null,
12307
+ "requirement": "every persisted run document MUST carry engineVersion; the \"MAY omit\" escape applies only to legacy runs that predate the contract, and this run was created moments ago",
12308
+ "interpolated": true
12309
+ },
12310
+ {
12311
+ "section": null,
12312
+ "requirement": null,
12313
+ "interpolated": true
12314
+ }
12315
+ ]
12316
+ },
12265
12317
  {
12266
12318
  "id": "openwop.it.errors.get-v1-runs-nonexistentid-returns-canonical-envelope",
12267
12319
  "file": "errors.test.ts",
@@ -26276,7 +26328,8 @@
26276
26328
  },
26277
26329
  {
26278
26330
  "section": "spec/v2/core/versioning.md §5",
26279
- "requirement": "the v2 read MUST name the same run"
26331
+ "requirement": null,
26332
+ "interpolated": true
26280
26333
  },
26281
26334
  {
26282
26335
  "section": "spec/v2/core/versioning.md §1.4",
@@ -26287,7 +26340,7 @@
26287
26340
  {
26288
26341
  "id": "openwop.it.v2-dual-stack-negotiation.an-unlisted-major-is-406-protocol-version-unsupported-with-details-protocolversi",
26289
26342
  "file": "v2-dual-stack-negotiation.test.ts",
26290
- "line": 83,
26343
+ "line": 108,
26291
26344
  "title": "an unlisted major is 406 protocol_version_unsupported with details.protocolVersions[]",
26292
26345
  "explicitId": "openwop.requirement.0172.dual-stack-negotiation.unlisted-major",
26293
26346
  "citations": [
@@ -26311,7 +26364,7 @@
26311
26364
  {
26312
26365
  "id": "openwop.it.v2-dual-stack-negotiation.openwop-version-2-0-on-a-v1-path-is-400-protocol-version-mismatch",
26313
26366
  "file": "v2-dual-stack-negotiation.test.ts",
26314
- "line": 94,
26367
+ "line": 119,
26315
26368
  "title": "OpenWOP-Version: 2.0 on a /v1/ path is 400 protocol_version_mismatch",
26316
26369
  "explicitId": "openwop.requirement.0172.dual-stack-negotiation.v1-path-mismatch",
26317
26370
  "citations": [
@@ -26330,7 +26383,7 @@
26330
26383
  {
26331
26384
  "id": "openwop.it.v2-dual-stack-negotiation.every-response-reports-the-contract-that-produced-it",
26332
26385
  "file": "v2-dual-stack-negotiation.test.ts",
26333
- "line": 103,
26386
+ "line": 128,
26334
26387
  "title": "every response reports the contract that produced it",
26335
26388
  "explicitId": "openwop.requirement.0172.dual-stack-negotiation.response-header",
26336
26389
  "citations": [
@@ -28425,7 +28478,7 @@
28425
28478
  {
28426
28479
  "id": "openwop.it.version-negotiation.get-well-known-openwop-returns-capabilities-with-protocolversion-string",
28427
28480
  "file": "version-negotiation.test.ts",
28428
- "line": 46,
28481
+ "line": 68,
28429
28482
  "title": "GET /.well-known/openwop returns Capabilities with protocolVersion (string)",
28430
28483
  "explicitId": "openwop.it.version-negotiation.get-well-known-openwop-returns-capabilities-with-protocolversion-string",
28431
28484
  "citations": [
@@ -28438,7 +28491,7 @@
28438
28491
  {
28439
28492
  "id": "openwop.it.version-negotiation.every-event-has-the-6-required-runeventdoc-fields-per-run-event-schema-json",
28440
28493
  "file": "version-negotiation.test.ts",
28441
- "line": 60,
28494
+ "line": 82,
28442
28495
  "title": "every event has the 6 required RunEventDoc fields per run-event.schema.json",
28443
28496
  "explicitId": "openwop.it.version-negotiation.every-event-has-the-6-required-runeventdoc-fields-per-run-event-schema-json",
28444
28497
  "citations": [
@@ -28479,7 +28532,7 @@
28479
28532
  {
28480
28533
  "id": "openwop.it.version-negotiation.event-sequences-within-a-run-are-strictly-monotonic",
28481
28534
  "file": "version-negotiation.test.ts",
28482
- "line": 104,
28535
+ "line": 126,
28483
28536
  "title": "event sequences within a run are strictly monotonic",
28484
28537
  "explicitId": "openwop.it.version-negotiation.event-sequences-within-a-run-are-strictly-monotonic",
28485
28538
  "citations": [
@@ -28493,7 +28546,7 @@
28493
28546
  {
28494
28547
  "id": "openwop.it.version-negotiation.events-poll-with-lastsequence-past-current-end-returns-empty-events-iscomplete",
28495
28548
  "file": "version-negotiation.test.ts",
28496
- "line": 132,
28549
+ "line": 154,
28497
28550
  "title": "events/poll with lastSequence past current end returns empty events + isComplete",
28498
28551
  "explicitId": "openwop.it.version-negotiation.events-poll-with-lastsequence-past-current-end-returns-empty-events-iscomplete",
28499
28552
  "citations": [
@@ -34543,7 +34596,7 @@
34543
34596
  {
34544
34597
  "id": "openwop.it.v2-bundle-signature-attributable.the-v2-discovery-root-publishes-signingkeys-the-surface-the-rfc-named",
34545
34598
  "file": "v2-bundle-signature-attributable.test.ts",
34546
- "line": 94,
34599
+ "line": 103,
34547
34600
  "title": "the v2 discovery root publishes signingKeys[] — the surface the RFC named",
34548
34601
  "explicitId": null,
34549
34602
  "citations": [
@@ -34572,7 +34625,7 @@
34572
34625
  {
34573
34626
  "id": "openwop.it.v2-bundle-signature-attributable.the-front-door-gate-verifies-the-attestation-instead-of-inspecting-the-string",
34574
34627
  "file": "v2-bundle-signature-attributable.test.ts",
34575
- "line": 125,
34628
+ "line": 134,
34576
34629
  "title": "the Front-door gate verifies the attestation instead of inspecting the string",
34577
34630
  "explicitId": null,
34578
34631
  "citations": [
@@ -34591,7 +34644,7 @@
34591
34644
  {
34592
34645
  "id": "openwop.it.v2-bundle-signature-attributable.the-four-signature-outcomes-stay-distinct-collapsing-any-two-restores-the-defect",
34593
34646
  "file": "v2-bundle-signature-attributable.test.ts",
34594
- "line": 140,
34647
+ "line": 149,
34595
34648
  "title": "the four signature outcomes stay distinct — collapsing any two restores the defect",
34596
34649
  "explicitId": null,
34597
34650
  "citations": [
@@ -34605,7 +34658,7 @@
34605
34658
  {
34606
34659
  "id": "openwop.it.v2-bundle-signature-attributable.signingkeys-is-reachable-from-a-v1-root-too-a-bundle-is-v3-regardless-of-major",
34607
34660
  "file": "v2-bundle-signature-attributable.test.ts",
34608
- "line": 161,
34661
+ "line": 170,
34609
34662
  "title": "signingKeys[] is reachable from a v1 root too — a bundle is v3 regardless of major",
34610
34663
  "explicitId": null,
34611
34664
  "citations": [
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
3
3
  "counts": {
4
- "files": 499,
5
- "v1": 444,
4
+ "files": 500,
5
+ "v1": 445,
6
6
  "v2": 56
7
7
  },
8
8
  "majors": {
@@ -564,6 +564,9 @@
564
564
  "envelope-variant-discriminator-static.test.ts": [
565
565
  1
566
566
  ],
567
+ "era-key-stamped-v1.test.ts": [
568
+ 1
569
+ ],
567
570
  "errors.test.ts": [
568
571
  1
569
572
  ],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.0.0-rc.19",
4
+ "version": "2.0.0-rc.28",
5
5
  "corpusTag": null,
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
@@ -9,9 +9,9 @@
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "954d7668c34a0f6132fe427f606af73999dd5b97971d1ce1f701ff8cbbf7191c",
13
- "api/v2/asyncapi.yaml": "5139025faefe214771b239a7d82b154eb225298d90017a20d28e642dc3a1d546",
14
- "api/v2/openapi.yaml": "b657a099dc5c36727ee6177face4fad66a5ac6b2110de8caa1d4ae6c1900948e",
12
+ "api/seams-v2.yaml": "d92f0832250f7e2943304c3ebb3b4ac4d94a627a5f275a8cd29f608cae48f920",
13
+ "api/v2/asyncapi.yaml": "917d25c0c03d418b43b778423182de1aaa2f7b4f30a175bdeab3b7d3ffdb894a",
14
+ "api/v2/openapi.yaml": "ccd9e6a5dd8084c6d6c0959157f447da96fc7493cad2479abbf97b69483f9404",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -86,7 +86,7 @@
86
86
  "schemas/run-event.schema.json": "c70c06932b9831295042836213e617c8e946f20ddd49cf4e889b74d89d2175a8",
87
87
  "schemas/run-options.schema.json": "f8241ece6ddc6527de9160d811d42c231b4f77b935f2e107b3c05608545c59ab",
88
88
  "schemas/run-orchestrator-decided-event.schema.json": "17a43a3c44680a24d411141357a867b3caed6107d56de88fe2e062eaf93fa2c1",
89
- "schemas/run-snapshot.schema.json": "8a45f997beec0a2fa596f4631129a869cfbc37c9ad38583530ed11655301d2ab",
89
+ "schemas/run-snapshot.schema.json": "3343550f6973a0975c61624df569d341e843ac8592a2aa2b3b7cf540d72563ad",
90
90
  "schemas/security-advisory.schema.json": "9c550f0bf10a3193dcca4a127081fb66fe5dc1b6f729c4c17bfaea3ba3ce05fc",
91
91
  "schemas/self-hosted-runner-dispatch-frame.schema.json": "3f1d7edd572c6747657299525d8e85329b93b6fadf670e3a19c74d53e725adec",
92
92
  "schemas/self-hosted-runner-registration.schema.json": "6c781adf1bb915a9e47796494f4a527353cbbf72b480e5a05be4aac65a6d0c6e",
@@ -198,7 +198,7 @@
198
198
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
199
199
  "spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
200
200
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
201
- "spec/v1/core-standard-manifest.json": "28497ea7f168ac18f721751e87ad5056579be070ef2088fa85d69282ea62cc23",
201
+ "spec/v1/core-standard-manifest.json": "bd06beb20cc52f8558e8b72598becb380e3159bb842f13389bcf92619f1fad23",
202
202
  "spec/v1/deprecations.json": "520d79927a8ed43ccf62c207a7cf6cf7b19b16e00bbbbdf0410667e939c19d85",
203
203
  "spec/v1/deprecations.schema.json": "4cdbbb8eb9c0092913a5704c943880f0c8581bcf0f640a7df5a84105d53c8aee",
204
204
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -228,7 +228,7 @@
228
228
  "spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
229
229
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
230
230
  "spec/v2/profiles.json": "1f0ee40491131cf561ef19a0394c47ba476b4567514fe16befb6cada4a10a5a4",
231
- "spec/v2/release.json": "eab43630e8b036b91589ffb275db1e172fda81c8a851fe31d833d9b54ac98e45"
231
+ "spec/v2/release.json": "20a6cb7efde5fcdbbc2b663976882b6dec93ec52556d422de7290e25906f2748"
232
232
  },
233
- "corpusCommit": "c8d036fb1c060a77d066de932f9ae3b6e9cf76c2"
233
+ "corpusCommit": "e9aa9131f3d64f814b2c0ca16535675968efad76"
234
234
  }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * `spec/v1/version-negotiation.md` §Stamping / §Legacy detection — the two
3
+ * run-document stamping MUSTs, and the legacy rule that makes their absence
4
+ * actively harmful (suite 2.0.0, target major 1; unaided).
5
+ *
6
+ * The rule is a v1 `MUST` and has been since the contract was written:
7
+ *
8
+ * §Stamping "Every persisted run document MUST carry an
9
+ * `eventLogSchemaVersion: number` field. The current v1
10
+ * value is `2`."
11
+ * §Legacy detection "Hosts identify an older run document as legacy when
12
+ * `eventLogSchemaVersion` is undefined or `< 2`" … legacy
13
+ * runs have "no event subcollection … Readers MUST fall
14
+ * back to the snapshot for state."
15
+ *
16
+ * **Nothing in the suite has ever asserted it.** `version-negotiation.test.ts`
17
+ * opens by claiming it checks "the four version axes (`engineVersion`,
18
+ * `eventLogSchemaVersion`, per-event `schemaVersion`, `pinnedVersions`) appear
19
+ * where the spec says they should" — and `protocolVersion` is the only axis it
20
+ * asserts. Across all 444 v1 scenario files the sole occurrence of the
21
+ * identifier `eventLogSchemaVersion` was that sentence: a docstring describing
22
+ * a check that does not exist. **A comment claiming coverage is worse than no
23
+ * comment**, because it answers "is this tested?" for anyone who greps, and
24
+ * answers it wrongly.
25
+ *
26
+ * Both production hosts were measured on 2026-09-04 and neither stamps the
27
+ * field on any run it has ever served. Each found it independently, after the
28
+ * other published its own greps.
29
+ *
30
+ * The consequence fails in the direction that punishes correctness. A client
31
+ * following §Legacy detection exactly classifies every such run as legacy and
32
+ * reads the snapshot — **ignoring the event log the host is in fact serving**.
33
+ * The host under-serves the conforming reader and over-serves the careless one.
34
+ *
35
+ * Why the schema could not catch it: `run-snapshot.schema.json` requires only
36
+ * `runId`, `workflowId` and `status`, so a snapshot missing the field validates
37
+ * cleanly. The obligation is prose-only, which is exactly the shape that needs
38
+ * a scenario rather than a keyword.
39
+ *
40
+ * @see spec/v1/version-negotiation.md §Stamping
41
+ * @see spec/v1/version-negotiation.md §Legacy detection
42
+ */
43
+
44
+ import { describe, it, expect } from 'vitest';
45
+ import { driver } from '../lib/driver.js';
46
+ import { softSkip } from '../lib/soft-skip.js';
47
+ import { req } from '../lib/requirement-ids.js';
48
+
49
+ const ID_STAMPED = 'openwop.requirement.version-negotiation.era-key-stamped';
50
+ const ID_NOT_LEGACY = 'openwop.requirement.version-negotiation.era-key-not-legacy';
51
+ const ID_ENGINE = 'openwop.requirement.version-negotiation.engine-version-stamped';
52
+ const DOC = 'spec/v1/version-negotiation.md §Stamping';
53
+
54
+ interface Snapshot { readonly eventLogSchemaVersion?: unknown; readonly engineVersion?: unknown }
55
+
56
+ /** A run this host created moments ago — the one case where "legacy" cannot apply. */
57
+ async function freshRun(): Promise<{ runId: string } | { skip: string }> {
58
+ try {
59
+ // v1 path keys are explicit: the driver's unversioned rewrite is a major-2
60
+ // behaviour, and `/runs` answers 404 on a v1 host. The first version of this
61
+ // file used `/runs` and therefore SOFT-SKIPPED against a host that violates
62
+ // the rule — passing vacuously, which is the failure this scenario exists to
63
+ // catch, committed by the scenario itself.
64
+ const created = await driver.post('/v1/runs', { workflowId: 'conformance-noop', inputs: {} });
65
+ if (created.status !== 201) return { skip: `POST /v1/runs answered ${created.status} — no run to inspect` };
66
+ const runId = (created.json as { runId?: unknown } | null)?.runId;
67
+ if (typeof runId !== 'string') return { skip: 'POST /v1/runs returned no runId' };
68
+ return { runId };
69
+ } catch {
70
+ return { skip: 'POST /v1/runs unreachable' };
71
+ }
72
+ }
73
+
74
+ describe('era-key-stamped-v1 (version-negotiation.md §Stamping)', () => {
75
+ it('a run the host just created carries eventLogSchemaVersion', async () => {
76
+ const r = await freshRun();
77
+ if ('skip' in r) return softSkip('blocked', r.skip);
78
+
79
+ let snap;
80
+ try {
81
+ snap = await driver.get(`/v1/runs/${encodeURIComponent(r.runId)}`);
82
+ } catch {
83
+ return softSkip('blocked', 'GET /v1/runs/{runId} unreachable');
84
+ }
85
+ if (snap.status !== 200) return softSkip('blocked', `GET /v1/runs/{runId} answered ${snap.status}`);
86
+
87
+ const value = (snap.json as Snapshot | null)?.eventLogSchemaVersion;
88
+ expect(
89
+ value,
90
+ req(ID_STAMPED, DOC, 'every persisted run document MUST carry an eventLogSchemaVersion — the field is prose-only (run-snapshot.schema.json requires just runId, workflowId and status), so a snapshot without it validates cleanly and only this check can see its absence'),
91
+ ).not.toBeUndefined();
92
+ expect(
93
+ typeof value === 'number',
94
+ req(ID_STAMPED, DOC, `eventLogSchemaVersion MUST be a number (got ${JSON.stringify(value)})`),
95
+ ).toBe(true);
96
+ });
97
+
98
+ it('a freshly created run is not classified legacy by the host\'s own rule', async () => {
99
+ const r = await freshRun();
100
+ if ('skip' in r) return softSkip('blocked', r.skip);
101
+
102
+ let snap;
103
+ try {
104
+ snap = await driver.get(`/v1/runs/${encodeURIComponent(r.runId)}`);
105
+ } catch {
106
+ return softSkip('blocked', 'GET /v1/runs/{runId} unreachable');
107
+ }
108
+ if (snap.status !== 200) return softSkip('blocked', `GET /v1/runs/{runId} answered ${snap.status}`);
109
+ const value = (snap.json as Snapshot | null)?.eventLogSchemaVersion;
110
+ if (value === undefined) {
111
+ return softSkip('blocked', 'the field is absent — the stamping leg above records that; legacy classification cannot be judged separately from it');
112
+ }
113
+
114
+ // §Legacy detection: "undefined or < 2" is legacy, and a legacy run means
115
+ // "no event subcollection … Readers MUST fall back to the snapshot". A host
116
+ // that serves an event log while stamping a legacy value is telling a
117
+ // conforming client to ignore the log it is serving.
118
+ expect(
119
+ typeof value === 'number' && value >= 2,
120
+ req(ID_NOT_LEGACY, 'spec/v1/version-negotiation.md §Legacy detection', `a run created moments ago MUST NOT be legacy: legacy is "undefined or < 2", and a legacy run is specified to have no event subcollection so readers MUST fall back to the snapshot. Stamping ${JSON.stringify(value)} on a new run instructs a CONFORMING client to ignore the event log this host is serving it — the failure lands on the correct reader and spares the careless one`),
121
+ ).toBe(true);
122
+ });
123
+
124
+ it('a run the host just created carries engineVersion — the legacy escape cannot reach it', async () => {
125
+ const r = await freshRun();
126
+ if ('skip' in r) return softSkip('blocked', r.skip);
127
+
128
+ let snap;
129
+ try {
130
+ snap = await driver.get(`/v1/runs/${encodeURIComponent(r.runId)}`);
131
+ } catch {
132
+ return softSkip('blocked', 'GET /v1/runs/{runId} unreachable');
133
+ }
134
+ if (snap.status !== 200) return softSkip('blocked', `GET /v1/runs/{runId} answered ${snap.status}`);
135
+
136
+ // §Stamping: "Every persisted run document MUST carry an `engineVersion:
137
+ // number` field … Servers MAY omit this field on legacy runs that predate
138
+ // the contract." The escape is scoped to runs that PREDATE the contract, so
139
+ // it cannot cover a run created seconds ago — which is why this leg creates
140
+ // one rather than inspecting whatever happens to be in the store.
141
+ //
142
+ // Asserted here because nothing else asserts it ON A RUN: version-fold.test.ts
143
+ // reads engineVersion from the DISCOVERY document, and wasm-pack-load.test.ts
144
+ // carries it only as a type field. Both mention the identifier, so a grep
145
+ // suggests coverage that does not exist for this requirement.
146
+ const value = (snap.json as Snapshot | null)?.engineVersion;
147
+ expect(
148
+ value,
149
+ req(ID_ENGINE, DOC, 'every persisted run document MUST carry engineVersion; the "MAY omit" escape applies only to legacy runs that predate the contract, and this run was created moments ago'),
150
+ ).not.toBeUndefined();
151
+ expect(
152
+ typeof value === 'number',
153
+ req(ID_ENGINE, DOC, `engineVersion MUST be a number set to the writer engine's CURRENT_ENGINE_VERSION at write time (got ${JSON.stringify(value)})`),
154
+ ).toBe(true);
155
+ });
156
+ });
@@ -76,7 +76,32 @@ describe('v2 dual-stack-negotiation (RFC 0172 §A.3–§A.4 — gated on two maj
76
76
  const read = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}`, { headers: { 'OpenWOP-Version': '2.0' } }));
77
77
  if (read === null) return softSkip('blocked', 'GET /runs/{runId} unreachable (fetch failed)');
78
78
  expect(read.status, req('openwop.requirement.0172.dual-stack-negotiation.cross-major-read', 'spec/v2/core/versioning.md §5', 'the overlap: a run created through /v1/runs MUST be readable through GET /runs/{runId} with OpenWOP-Version: 2.0')).toBe(200);
79
- expect((read.json as { runId?: unknown } | undefined)?.runId, req('openwop.requirement.0172.dual-stack-negotiation.cross-major-read', 'spec/v2/core/versioning.md §5', 'the v2 read MUST name the same run')).toBe(runId);
79
+ // NAMES the same run not byte-identical to the v1 id.
80
+ //
81
+ // This asserted `.toBe(runId)` until 2026-09-04, which invented a
82
+ // requirement. `versioning.md` §5 describes the dual-stack scenario's SHAPE
83
+ // ("creates one run through /v1/runs … and reads it through /runs with
84
+ // OpenWOP-Version: 2; the response headers name the contract used") and says
85
+ // nothing about the identifier. `identity.md` §5 does: under major 2 a
86
+ // `runId` is tenant-bound `<tenantId>/<opaque>`, a grammar in
87
+ // `ids.schema.json`. A host implementing BOTH sections faithfully cannot
88
+ // satisfy byte-equality, and one turned its P4-D branch red on exactly this.
89
+ //
90
+ // The assertion tighter than its own prose is the mirror of the schema
91
+ // looser than its own prose: both are gaps between a check and the text it
92
+ // cites, and both are invisible until a host implements the text.
93
+ //
94
+ // What §5 actually requires is that the v2 read resolves to the SAME RUN.
95
+ // Accept the identical id (a host that does not project) or the tenant-bound
96
+ // projection of it (a host that does); reject anything else, which would be
97
+ // a different run.
98
+ const readId = (read.json as { runId?: unknown } | undefined)?.runId;
99
+ const namesSameRun =
100
+ readId === runId || (typeof readId === 'string' && readId.endsWith(`/${runId}`) && readId.split('/').length === 2);
101
+ expect(
102
+ namesSameRun,
103
+ req('openwop.requirement.0172.dual-stack-negotiation.cross-major-read', 'spec/v2/core/versioning.md §5', `the v2 read MUST name the same run: either the v1 id verbatim, or its tenant-bound projection <tenantId>/${'${runId}'} per identity.md §5. Got ${JSON.stringify(readId)} for a run created as ${JSON.stringify(runId)}`),
104
+ ).toBe(true);
80
105
  expect(major(read), req('openwop.requirement.0172.dual-stack-negotiation.cross-major-read', 'spec/v2/core/versioning.md §1.4', 'the v2 read MUST report the 2.x contract that produced it')).toBe('2');
81
106
  });
82
107
 
@@ -7,9 +7,31 @@
7
7
  *
8
8
  * What we CAN test cheaply:
9
9
  * 1. Server advertises a `protocolVersion` in `Capabilities`.
10
- * 2. The four version axes (`engineVersion`,
11
- * `eventLogSchemaVersion`, per-event `schemaVersion`,
12
- * `pinnedVersions`) appear where the spec says they should.
10
+ * 2. `protocolVersion` is advertised, and every event carries the six
11
+ * required `RunEventDoc` fields.
12
+ *
13
+ * This file previously claimed to check "the four version axes
14
+ * (`engineVersion`, `eventLogSchemaVersion`, per-event `schemaVersion`,
15
+ * `pinnedVersions`)". IT DID NOT. `protocolVersion` was the only axis
16
+ * asserted, and across all 444 v1 scenario files the sole occurrence of the
17
+ * identifier `eventLogSchemaVersion` was that sentence — a docstring
18
+ * describing a check that did not exist. A comment claiming coverage is
19
+ * worse than no comment: it answers "is this tested?" for anyone who greps,
20
+ * and answers it wrongly.
21
+ *
22
+ * Current state of the four, stated so this comment can be checked rather
23
+ * than trusted: `eventLogSchemaVersion` and `engineVersion` are witnessed by
24
+ * `era-key-stamped-v1.test.ts` (both are run-document `MUST`s in
25
+ * `version-negotiation.md` §Stamping, and both were unasserted until
26
+ * 2026-09-04). Per-event `schemaVersion` and `pinnedVersions` are **not
27
+ * asserted here and carry no `MUST` in that document** — checked, rather
28
+ * than assumed to be a gap.
29
+ *
30
+ * This paragraph was itself wrong for one release candidate: it said
31
+ * `engineVersion` "remains UNASSERTED" after the leg asserting it had
32
+ * landed. A docstring that describes coverage goes stale the moment
33
+ * coverage changes, which is the argument for stating what can be
34
+ * re-derived rather than what was true once.
13
35
  * 3. Forward-compat read: events carrying an UNKNOWN
14
36
  * `schemaVersion` SHOULD still be readable via the events/poll
15
37
  * endpoint without 5xx (best-effort fold per