@openwop/openwop-conformance 2.3.0 → 2.3.2
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 +34 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +71 -4
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +19 -18
- package/src/scenarios/v2-webhook-delivery-shape.test.ts +237 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.3.2] — 2026-09-17 — the webhook reader read the wrong field
|
|
4
|
+
|
|
5
|
+
**Why a patch.** No scenario is added or removed; one is corrected. The 2.3.1 file asserted `event.owner` on a `run.completed` delivery — the envelope is closed and names no `owner`, and that payload carries none — so leg 1 was unsatisfiable and leg 3 vacuous on every host. Found by the reference host's W4 cut.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`v2-webhook-delivery-shape.test.ts`** — subscribes to `run.started`, whose
|
|
10
|
+
payload is where the owner echo lives (`runStarted.owner`). Leg 1 validates the
|
|
11
|
+
delivery against `webhook-delivery.schema.json` (v2) and the payload against the
|
|
12
|
+
v2 `runStarted` definition; the owner carries `subject`, never `principal`.
|
|
13
|
+
Leg 2 registers at `POST /v1/webhooks` (`versioning.md` §1.4) and validates the
|
|
14
|
+
payload against the **v1** `runStarted` definition — the owner's keys are not
|
|
15
|
+
the discriminator (v1 admits `subject`, RFC 0165 §B); the integer-vs-string
|
|
16
|
+
`engineVersion` is. A host with no 1.x webhook surface records leg 2
|
|
17
|
+
`inapplicable`. Leg 3 reads the seeded era-2 run's `run.started`; a host that
|
|
18
|
+
does not fan out seeded history records it `inapplicable`. Two 2.3.1 gates were
|
|
19
|
+
vacuous and are fixed: leg 2 read `versions.supported` (no such field — it is
|
|
20
|
+
`protocolVersions`), leg 3 read `era2Gate`'s `null` (= seam advertised) as
|
|
21
|
+
absence. Same requirement id, same gate.
|
|
22
|
+
|
|
23
|
+
## [2.3.1] — 2026-09-17 — the webhook body is finally read
|
|
24
|
+
|
|
25
|
+
**Why a patch.** One scenario file is added, which `PROTOCOL-STATUS.md` calls a minor — but the previous cut (2.3.0) was a minor ninety minutes ago and this file witnesses a MUST that predates it; cut as a patch alongside the register and ext-rule edits it ships with. If that reads as bending the rule, the rule wins and this becomes 2.4.0 on review.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`v2-webhook-delivery-shape.test.ts`** — gated on the `webhooks` family. A major-2
|
|
30
|
+
subscriber's delivery body validates against `run-event.schema.json` (v2) and
|
|
31
|
+
`owner` is exactly `{tenant, subject}`; a major-1 subscriber still receives
|
|
32
|
+
`principal`; a seeded era-2 run (seam-gated) is delivered projected. The
|
|
33
|
+
second leg is the one that keeps the first honest: projecting BOTH channels
|
|
34
|
+
to v2 is as wrong as projecting none. Sabotage on the reporting host: drop
|
|
35
|
+
the fan-out projection → leg 1 reds, leg 2 stays green.
|
|
36
|
+
|
|
3
37
|
## [2.3.0] — 2026-09-17 — the three RFC 0186 payload seats are witnessed
|
|
4
38
|
|
|
5
39
|
**Why a minor.** `PROTOCOL-STATUS.md` §suite version: minor on scenario add/remove. One scenario file is added.
|
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
|
|
|
117
117
|
|
|
118
118
|
## What's Covered
|
|
119
119
|
|
|
120
|
-
The current suite has
|
|
120
|
+
The current suite has 521 scenario files under `src/scenarios/`.
|
|
121
121
|
- 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.
|
|
122
122
|
- 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.
|
|
123
123
|
- 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.**
|
|
@@ -462,7 +462,7 @@ Server-required (added in 1.7.0):
|
|
|
462
462
|
| ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
463
463
|
| **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. |
|
|
464
464
|
|
|
465
|
-
Current source tree:
|
|
465
|
+
Current source tree: 521 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
|
|
466
466
|
|
|
467
467
|
## Remaining Gaps
|
|
468
468
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
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.3.
|
|
59
|
+
"@openwop/spec-artifacts": "2.3.2"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 570,
|
|
6
|
+
"tests": 2168,
|
|
7
|
+
"withStableId": 2168,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2123
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -29226,6 +29226,73 @@
|
|
|
29226
29226
|
}
|
|
29227
29227
|
]
|
|
29228
29228
|
},
|
|
29229
|
+
{
|
|
29230
|
+
"id": "openwop.it.v2-webhook-delivery-shape.a-major-2-subscriber-receives-the-v2-rendering-the-delivery-validates-and-run-st",
|
|
29231
|
+
"file": "v2-webhook-delivery-shape.test.ts",
|
|
29232
|
+
"line": 172,
|
|
29233
|
+
"title": "a major-2 subscriber receives the v2 rendering: the delivery validates, and run.started.owner carries subject, never principal",
|
|
29234
|
+
"explicitId": "openwop.requirement.0171.webhook-delivery-shape",
|
|
29235
|
+
"citations": [
|
|
29236
|
+
{
|
|
29237
|
+
"section": "spec/v2/core/webhooks.md §Delivery",
|
|
29238
|
+
"requirement": null,
|
|
29239
|
+
"interpolated": true
|
|
29240
|
+
},
|
|
29241
|
+
{
|
|
29242
|
+
"section": "spec/v2/core/webhooks.md §Delivery",
|
|
29243
|
+
"requirement": null,
|
|
29244
|
+
"interpolated": true
|
|
29245
|
+
},
|
|
29246
|
+
{
|
|
29247
|
+
"section": "spec/v2/core/webhooks.md §Delivery",
|
|
29248
|
+
"requirement": "a major-2 run.started payload MUST carry the owner echo { tenant, subject } (identity.md §1; run-event-payloads runStarted.owner requires both)"
|
|
29249
|
+
},
|
|
29250
|
+
{
|
|
29251
|
+
"section": "spec/v2/core/webhooks.md §Delivery",
|
|
29252
|
+
"requirement": "a major-2 owner echo MUST NOT carry the v1 keys principal / principalKind — that is the fan-out forwarding the in-process dialect instead of projecting"
|
|
29253
|
+
},
|
|
29254
|
+
{
|
|
29255
|
+
"section": "spec/v2/core/webhooks.md §Delivery",
|
|
29256
|
+
"requirement": "a major-2 owner echo carries tenant and subject"
|
|
29257
|
+
}
|
|
29258
|
+
]
|
|
29259
|
+
},
|
|
29260
|
+
{
|
|
29261
|
+
"id": "openwop.it.v2-webhook-delivery-shape.a-major-1-subscriber-still-receives-the-v1-rendering-the-v1-wire-does-not-move-m",
|
|
29262
|
+
"file": "v2-webhook-delivery-shape.test.ts",
|
|
29263
|
+
"line": 191,
|
|
29264
|
+
"title": "a major-1 subscriber still receives the v1 rendering — the v1 wire does not move mid-overlap",
|
|
29265
|
+
"explicitId": "openwop.requirement.0171.webhook-delivery-shape",
|
|
29266
|
+
"citations": [
|
|
29267
|
+
{
|
|
29268
|
+
"section": "spec/v2/core/versioning.md §1.2",
|
|
29269
|
+
"requirement": null,
|
|
29270
|
+
"interpolated": true
|
|
29271
|
+
},
|
|
29272
|
+
{
|
|
29273
|
+
"section": "spec/v2/core/versioning.md §1.2",
|
|
29274
|
+
"requirement": "a v1 owner echo, when present, names the tenant (v1 runStarted.owner requires it)"
|
|
29275
|
+
}
|
|
29276
|
+
]
|
|
29277
|
+
},
|
|
29278
|
+
{
|
|
29279
|
+
"id": "openwop.it.v2-webhook-delivery-shape.a-seeded-era-2-run-is-delivered-projected-too-the-fan-out-branch-a-fresh-run-can",
|
|
29280
|
+
"file": "v2-webhook-delivery-shape.test.ts",
|
|
29281
|
+
"line": 214,
|
|
29282
|
+
"title": "a seeded era-2 run is delivered projected too — the fan-out branch a fresh run cannot reach (seam-gated)",
|
|
29283
|
+
"explicitId": "openwop.requirement.0171.webhook-delivery-shape",
|
|
29284
|
+
"citations": [
|
|
29285
|
+
{
|
|
29286
|
+
"section": "spec/v2/core/webhooks.md §Delivery",
|
|
29287
|
+
"requirement": null,
|
|
29288
|
+
"interpolated": true
|
|
29289
|
+
},
|
|
29290
|
+
{
|
|
29291
|
+
"section": "spec/v2/core/webhooks.md §Delivery",
|
|
29292
|
+
"requirement": "a projected era-2 delivery MUST NOT carry the v1 owner keys"
|
|
29293
|
+
}
|
|
29294
|
+
]
|
|
29295
|
+
},
|
|
29229
29296
|
{
|
|
29230
29297
|
"id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
|
|
29231
29298
|
"file": "v2-webhook-durable-delivery.test.ts",
|
package/scenario-majors.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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":
|
|
4
|
+
"files": 521,
|
|
5
5
|
"v1": 445,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 78
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1451,6 +1451,9 @@
|
|
|
1451
1451
|
"v2-version-header-honored.test.ts": [
|
|
1452
1452
|
2
|
|
1453
1453
|
],
|
|
1454
|
+
"v2-webhook-delivery-shape.test.ts": [
|
|
1455
|
+
2
|
|
1456
|
+
],
|
|
1454
1457
|
"v2-webhook-durable-delivery.test.ts": [
|
|
1455
1458
|
2
|
|
1456
1459
|
],
|
|
@@ -1,17 +1,17 @@
|
|
|
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.3.
|
|
5
|
-
"corpusTag": "v2.3.
|
|
4
|
+
"version": "2.3.2",
|
|
5
|
+
"corpusTag": "v2.3.2",
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
8
8
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
|
|
11
11
|
"api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
|
|
12
|
-
"api/seams-v2.yaml": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "7c79a7846a32c54f4281bf62dc00f7fa98d512da21d5665f29ab1e184eecb07e",
|
|
13
|
+
"api/v2/asyncapi.yaml": "ca998bb1cfc15d0f43ca113e329eec688fcaf8cfa058d9d146678baa6a7ae88b",
|
|
14
|
+
"api/v2/openapi.yaml": "107e30840cc96f0ba8ff01bc84614b4e6d5ca59502141b586acc1f9c67fc7ac1",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
"schemas/v2/envelopes/schema.request.schema.json": "be2c5f7aa881cdd252ccff47ac02193659e1393633a1541f4297383205d384a2",
|
|
142
142
|
"schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
|
|
143
143
|
"schemas/v2/envelopes/ui.a2ui-surface.schema.json": "0a06fbbdc8ba797a6818c79336a98e211ac5ba5747945b4e4098072cd3974d79",
|
|
144
|
-
"schemas/v2/error-envelope.schema.json": "
|
|
144
|
+
"schemas/v2/error-envelope.schema.json": "9c8f747e5bea81943d70e0930bc7d26586a4c3a3d90be77f46359b19b7e56cbe",
|
|
145
145
|
"schemas/v2/eval-summary.schema.json": "9d31025752d0f67ad3b8a32e82a5314972c530ef9f780dd82e484ae8c5131198",
|
|
146
146
|
"schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
|
|
147
147
|
"schemas/v2/form-content-pack-manifest.schema.json": "307884e57946703b6f86ec283034bf56beb68bea4a000743ebbc9eebeed31b07",
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
"schemas/v2/residency.schema.json": "3fd23c9732beb140504e81560fc426eb6764d547e56da5fb92c5a5c7bf04db3a",
|
|
170
170
|
"schemas/v2/run-ancestry-response.schema.json": "334c17f2062dea62726ca1d07b8be3f169cdc422af843c1def5fb317039814c9",
|
|
171
171
|
"schemas/v2/run-diff-response.schema.json": "8bdb2045c274580038bd74781f7d062f5049839797c164a89de4ed89edc53a74",
|
|
172
|
-
"schemas/v2/run-event-payloads.schema.json": "
|
|
172
|
+
"schemas/v2/run-event-payloads.schema.json": "29dec16f0d40c9022c169d4d413742ddd775a3d8c895d3a60ccc093e2a872439",
|
|
173
173
|
"schemas/v2/run-event.schema.json": "1a9946374af9323b71635d00fed22abe02c300fa2cf3ae3e9c30f3e553e408ec",
|
|
174
174
|
"schemas/v2/run-list-response.schema.json": "c2dbc34768dcf64528914a213aa3cb55952e8e3e90a64b04a9423b2a19749d38",
|
|
175
175
|
"schemas/v2/run-options.schema.json": "f0e9f494d80086c2ed2cf9fe87df54e5b4e66601699c4a1089f4edbc7ba58867",
|
|
@@ -200,8 +200,8 @@
|
|
|
200
200
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
201
201
|
"spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
|
|
202
202
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
203
|
-
"spec/v1/core-standard-manifest.json": "
|
|
204
|
-
"spec/v1/deprecations.json": "
|
|
203
|
+
"spec/v1/core-standard-manifest.json": "bc852483ed0fe13b8d1898aa146794b05ab370d5c336bd26a8d25b25ae69b464",
|
|
204
|
+
"spec/v1/deprecations.json": "508071308e762c281807b487e8f3840a1a69b978071894593e18f2e2d3e4b5c0",
|
|
205
205
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
206
206
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
207
207
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
@@ -216,8 +216,8 @@
|
|
|
216
216
|
"spec/v2/core/capabilities.md": "c5107a4964e487c20bbb5254cfe1f706e0d9ba7e34da9070bc43b50b385e7948",
|
|
217
217
|
"spec/v2/core/conformance.md": "622084e3c74c4549087f4d861b1bb98cec201a0c5aec23788d27f53a355fde3b",
|
|
218
218
|
"spec/v2/core/connection-packs.md": "1d759681e2962f5103228c52c45f24607ed4bb9ae73e30605f690c1d5a59e9ee",
|
|
219
|
-
"spec/v2/core/errors.md": "
|
|
220
|
-
"spec/v2/core/events.md": "
|
|
219
|
+
"spec/v2/core/errors.md": "ad85fc830139e587f20deabf88297b91b1fb6ce31e80d5442624a650246ab16e",
|
|
220
|
+
"spec/v2/core/events.md": "20112f459e7f1bcab0e93fbfc2e28719ecd1435228e87c5476b9eaa691558603",
|
|
221
221
|
"spec/v2/core/form-content-packs.md": "6ffaae85d4550be48dd53fb6e2959af8aceb35bd87c5343b722885084f197713",
|
|
222
222
|
"spec/v2/core/headers.md": "2dd67d71673539e16612f09f3f2513a384f6566052d7cd8a00885885689854c2",
|
|
223
223
|
"spec/v2/core/idempotency.md": "b86c352d964aebad532727f314e26ed2177d49de382076fa64d3162e2f56118d",
|
|
@@ -235,9 +235,10 @@
|
|
|
235
235
|
"spec/v2/core/workflow-chain-packs.md": "ef2b07d2d99334eaec0cc370314e2ad2f84ea58deb699e5d92e6e52d6056613f",
|
|
236
236
|
"spec/v2/declaration.json": "a09bac61c1b825e4a77b5abc0f86f1f8c20c381e0650eb08c2f6d0ef0aaec1dc",
|
|
237
237
|
"spec/v2/declaration.schema.json": "c86e3083fb9789c1958ecb4576cc6f71aa90e6f339521139238543ed08211dd4",
|
|
238
|
-
"spec/v2/errors.json": "
|
|
238
|
+
"spec/v2/errors.json": "4ff032033f02d7e3fb4204581456408ba5e7642b438c1f349bbd1c264e63e419",
|
|
239
239
|
"spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
|
|
240
240
|
"spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
|
|
241
|
+
"spec/v2/ext/README.md": "c7dea12c48772047f18b111e73be1c236460e72d9bf98026351d148a2ac4f608",
|
|
241
242
|
"spec/v2/ext/a2uiSurface/README.md": "176cb0542e1c236ffcd6d682536ba0081242684444e2dec790abd480a44a571e",
|
|
242
243
|
"spec/v2/ext/brand/README.md": "1a80102a9cd766dcb6098c999e552af88f5b83c88951acde18d17cc0b29aa09d",
|
|
243
244
|
"spec/v2/ext/canvas/README.md": "a2d5f28523753e8e85cc9042db39faf9207a2eebce1a8553be22e341f7ea58d9",
|
|
@@ -245,16 +246,16 @@
|
|
|
245
246
|
"spec/v2/ext/coordination/README.md": "a193aa9a59a2d9ac8d2e9775e5be40c29b6f7476ffef35231dbab49377af9207",
|
|
246
247
|
"spec/v2/ext/dataIntegration/README.md": "8361c626ed9f77dc48a3bcaa0c9f769cb6706632941014700f7683efe0918bd1",
|
|
247
248
|
"spec/v2/ext/entities/README.md": "c3bf6c59ddf2255c22a522e689f6e7a11646fbcd309b17699b6cf69a4e2e0f91",
|
|
248
|
-
"spec/v2/ext/grpc-transport/README.md": "
|
|
249
|
+
"spec/v2/ext/grpc-transport/README.md": "9913c3f5433b719533e0721af477917bf7a993372388aa800be8bff9d4f8e203",
|
|
249
250
|
"spec/v2/ext/kanban/README.md": "6792926f7f0cb8a4dcb0acdedb86a1988574fb0b5de164b37159dfe3ba35f908",
|
|
250
251
|
"spec/v2/ext/knowledge/README.md": "d2615320c7de4aec279f276381376a8881e962dd4e86b1435462d742dd00a52f",
|
|
251
252
|
"spec/v2/ext/launchStudio/README.md": "bfe417e774689773f3bbced9fdfa0c18809577aa6040a1312d914c821beed026",
|
|
252
253
|
"spec/v2/ext/messaging/README.md": "8a4702935483ce7900c0ee14793f9dfbeeb09d5e43f8943c946f8f94de2efa6f",
|
|
253
|
-
"spec/v2/ext/portability/README.md": "
|
|
254
|
-
"spec/v2/ext/provider-idempotency/README.md": "
|
|
254
|
+
"spec/v2/ext/portability/README.md": "7525fdc595b05dfd0b2bd22e2f7b3a13302164ebf49e4327c2e29e9da80766f4",
|
|
255
|
+
"spec/v2/ext/provider-idempotency/README.md": "5efa5b220e22d8f9a93918d20d3be0c698445d828693c8e487f37abc39bfca7c",
|
|
255
256
|
"spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
|
|
256
257
|
"spec/v2/ext/restTransport/README.md": "b39f4d1ddbabd72dd945a39e90218d8413e46baea1269798b96dffb4e429cb4f",
|
|
257
|
-
"spec/v2/ext/sandbox-runtime-notes/README.md": "
|
|
258
|
+
"spec/v2/ext/sandbox-runtime-notes/README.md": "bd0c4ca4d0fbcba2ae948a02ddc768f50b08a33d9f0622efa1c9e2b7beab2969",
|
|
258
259
|
"spec/v2/ext/webResearch/README.md": "941dfe8323fd536791167f9fc7d119dd4e165bb7937e3382a19f15cda3d9f19a",
|
|
259
260
|
"spec/v2/facets/a2a.schema.json": "6b0bceddec6d18d5115aa7d04720a0cc1ad7a9a0a78f53c208b34becaba5d16e",
|
|
260
261
|
"spec/v2/facets/aiProviders.schema.json": "0c76e48e5d487b5be3b3ab1f6512a4e206c840ee5087d1336c7d7e2853b58d03",
|
|
@@ -270,8 +271,8 @@
|
|
|
270
271
|
"spec/v2/path-manifest.json": "a6f4e652e866889e8bde815874062f62929aee95dc8e58e7152c56adee262163",
|
|
271
272
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
272
273
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
273
|
-
"spec/v2/release.json": "
|
|
274
|
+
"spec/v2/release.json": "7b9a9aa90aa93dcf69076cd0a47c08043f6cf02b104280ab906deb0058c2caad",
|
|
274
275
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
275
276
|
},
|
|
276
|
-
"corpusCommit": "
|
|
277
|
+
"corpusCommit": "bad44bb22b53e538ec05e867299c4c2dddbfee5f"
|
|
277
278
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec/v2/core/webhooks.md` §Delivery — a webhook delivery's `event` is the
|
|
3
|
+
* verbatim run event AS THE SUBSCRIBER'S CONTRACT RENDERS IT (suite 2.3.2,
|
|
4
|
+
* target major 2; gated on the `webhooks` family; creates one run per leg).
|
|
5
|
+
*
|
|
6
|
+
* `webhooks.md:22`: "The delivery envelope is generated from the same payload
|
|
7
|
+
* definition as the event itself and the CloudEvents mapping — one source,
|
|
8
|
+
* three renderings (RFC 0171 §A.4)." That was normative before this file
|
|
9
|
+
* existed. What did not exist was a reader: seven webhook scenarios checked
|
|
10
|
+
* signature, headers, durability, isolation and SSRF, and NONE opened the body
|
|
11
|
+
* against a schema. A tier-1 host had two major-2 egress channels; poll/SSE
|
|
12
|
+
* projected the payload and the webhook fan-out forwarded the raw in-process
|
|
13
|
+
* event, so a major-2 subscriber received the v1 owner block (`principal`,
|
|
14
|
+
* `principalKind`) — with every webhook scenario green. The wire truth is per
|
|
15
|
+
* channel, and a channel nobody's scenario reads is a channel that can drift.
|
|
16
|
+
*
|
|
17
|
+
* WHERE THE OWNER LIVES (the 2.3.1 → 2.3.2 correction). The owner echo is a
|
|
18
|
+
* property of the `run.started` PAYLOAD — `run-event-payloads.schema.json`
|
|
19
|
+
* `runStarted.owner`, `{ tenant, workspace?, subject }` on the v2 wire and
|
|
20
|
+
* `{ tenant, workspace?, principal?, principalKind? }` on the v1 wire, both
|
|
21
|
+
* `additionalProperties: false` — not of the event envelope, which is closed
|
|
22
|
+
* and names no `owner` in either major. 2.3.1 subscribed to `run.completed`
|
|
23
|
+
* (whose payload is `{ outputs, durationMs }`) and read `event.owner`: a
|
|
24
|
+
* predicate no host could satisfy, and a third leg that passed vacuously.
|
|
25
|
+
* This file subscribes to `run.started` and reads `event.payload.owner`.
|
|
26
|
+
*
|
|
27
|
+
* Three legs, and the SECOND is the one that keeps the first honest. Leg 1: a
|
|
28
|
+
* major-2 subscription's delivery validates against `webhook-delivery.schema.json`
|
|
29
|
+
* (v2), its `run.started` payload against the v2 `runStarted` definition, and
|
|
30
|
+
* the owner carries `subject` and never `principal`. Leg 2: a major-1
|
|
31
|
+
* subscription's `run.started` payload validates against the V1 `runStarted`
|
|
32
|
+
* definition — `versioning.md` §1.2 forbids moving the v1 wire mid-overlap, so
|
|
33
|
+
* a host that projects BOTH channels to v2 is also wrong, and a v2-only check
|
|
34
|
+
* would reward it. The owner's keys are NOT the discriminator: v1's owner admits
|
|
35
|
+
* `subject` (RFC 0165 §B) beside `principal`, so a v2 owner is a valid v1 owner;
|
|
36
|
+
* the v2 payload's integer `engineVersion` (string on v1) is what the v1 wire
|
|
37
|
+
* cannot carry, and the v1 definition catches it. Leg 3 is
|
|
38
|
+
* seam-gated: the tier-2 host's fan-out is ERA-aware, not major-aware — an
|
|
39
|
+
* era-3 row was projected on write and passes through; only era-2 rows take the
|
|
40
|
+
* read projection — so a single fresh (era-3) run witnesses one branch. A
|
|
41
|
+
* seeded era-2 run witnesses the other.
|
|
42
|
+
*
|
|
43
|
+
* @see spec/v2/core/webhooks.md §Delivery
|
|
44
|
+
* @see spec/v2/core/versioning.md §1.2
|
|
45
|
+
*/
|
|
46
|
+
import { afterEach, describe, it, expect } from 'vitest';
|
|
47
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
48
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
49
|
+
import { join } from 'node:path';
|
|
50
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
51
|
+
import addFormats from 'ajv-formats';
|
|
52
|
+
import { driver } from '../lib/driver.js';
|
|
53
|
+
import { v2Discovery, gateFamily } from '../lib/v2.js';
|
|
54
|
+
import { receiverBinding, resolveRegistrationUrl } from '../lib/webhook-receiver.js';
|
|
55
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
56
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
57
|
+
import { req } from '../lib/requirement-ids.js';
|
|
58
|
+
import { era2Gate, seedEra2Log, v1FixtureLog } from '../lib/era2-seed.js';
|
|
59
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
60
|
+
|
|
61
|
+
const ID = 'openwop.requirement.0171.webhook-delivery-shape';
|
|
62
|
+
const DOC = 'spec/v2/core/webhooks.md §Delivery';
|
|
63
|
+
const FIXTURE = 'conformance-noop';
|
|
64
|
+
/** The one event type whose payload carries the owner echo — the field that differs between the two wires. */
|
|
65
|
+
const EVENT_TYPE = 'run.started';
|
|
66
|
+
|
|
67
|
+
type Delivery = { body: string; headers: Record<string, string | string[] | undefined> };
|
|
68
|
+
type Validator = (doc: unknown) => { ok: boolean; errors: string };
|
|
69
|
+
|
|
70
|
+
async function startReceiver(): Promise<{ server: Server; url: string; deliveries: Delivery[] }> {
|
|
71
|
+
const deliveries: Delivery[] = [];
|
|
72
|
+
const server = createServer((request: IncomingMessage, res: ServerResponse) => {
|
|
73
|
+
const chunks: Buffer[] = [];
|
|
74
|
+
request.on('data', (c: Buffer) => chunks.push(c));
|
|
75
|
+
request.on('end', () => {
|
|
76
|
+
deliveries.push({ body: Buffer.concat(chunks).toString('utf8'), headers: request.headers });
|
|
77
|
+
res.writeHead(204); res.end();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
const { bind, advertise } = receiverBinding();
|
|
81
|
+
await new Promise<void>((resolve) => server.listen(0, bind, () => resolve()));
|
|
82
|
+
const addr = server.address();
|
|
83
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
84
|
+
return { server, url: `http://${advertise}:${port}/hook`, deliveries };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Register a subscription; `major` selects the contract the host speaks to this
|
|
89
|
+
* subscriber. A 1.x registration goes to `/v1/webhooks` — v1 operations keep
|
|
90
|
+
* their `/v1/` path keys through the overlap (versioning.md §1.4) — and a host
|
|
91
|
+
* that serves no 1.x webhook surface answers 404 there, which is `inapplicable`
|
|
92
|
+
* for leg 2, not a failure: there is no v1 wire to keep still.
|
|
93
|
+
*/
|
|
94
|
+
async function register(url: string, major: 1 | 2): Promise<string | null> {
|
|
95
|
+
const registration = resolveRegistrationUrl(url);
|
|
96
|
+
const reg = await driver.post(major === 2 ? '/webhooks' : '/v1/webhooks', { url: registration.url, events: [EVENT_TYPE] }, { headers: { 'OpenWOP-Version': major === 2 ? '2.0' : '1.0' } });
|
|
97
|
+
if (major === 1 && reg.status === 404) {
|
|
98
|
+
softSkip('inapplicable', 'host serves no 1.x webhook surface (POST /v1/webhooks not_found) — no v1 wire to keep still');
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (reg.status === 400 && readErrorCode(reg.json) === 'webhook_url_rejected') {
|
|
102
|
+
softSkip('blocked', 'host SSRF guard rejected the loopback receiver (webhooks.md §Egress requires it); set OPENWOP_WEBHOOK_RECEIVER_URL to a public https receiver to witness');
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
expect(reg.status, req(ID, 'webhooks.md §Surfaces', 'POST /webhooks MUST answer 201 { webhookId }')).toBe(201);
|
|
106
|
+
const id = (reg.json as { webhookId?: unknown } | null)?.webhookId;
|
|
107
|
+
return typeof id === 'string' ? id : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function driveRun(): Promise<string> {
|
|
111
|
+
const create = await driver.post('/runs', { workflowId: FIXTURE });
|
|
112
|
+
expect(create.status, req(ID, 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
|
|
113
|
+
return (create.json as { runId: string }).runId;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function waitFor<T>(fn: () => T | undefined, ms: number): Promise<T | undefined> {
|
|
117
|
+
const until = Date.now() + ms;
|
|
118
|
+
while (Date.now() < until) { const v = fn(); if (v !== undefined) return v; await new Promise((r) => setTimeout(r, 200)); }
|
|
119
|
+
return fn();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The delivery for `runId` carrying the subscribed type. A v1 subscriber may
|
|
124
|
+
* receive the bare opaque id (versioning.md §5), so the match is on the segment
|
|
125
|
+
* both spellings share.
|
|
126
|
+
*/
|
|
127
|
+
function deliveryFor(deliveries: Delivery[], runId: string): { event: Record<string, unknown>; envelope: Record<string, unknown> } | undefined {
|
|
128
|
+
const opaque = runId.includes('/') ? runId.slice(runId.indexOf('/') + 1) : runId;
|
|
129
|
+
for (const d of deliveries) {
|
|
130
|
+
if (!d.body.includes(opaque)) continue;
|
|
131
|
+
let parsed: unknown;
|
|
132
|
+
try { parsed = JSON.parse(d.body); } catch { continue; }
|
|
133
|
+
if (parsed === null || typeof parsed !== 'object') continue;
|
|
134
|
+
const envelope = parsed as Record<string, unknown>;
|
|
135
|
+
const event = (envelope['event'] ?? envelope) as Record<string, unknown>;
|
|
136
|
+
if (event['type'] === EVENT_TYPE) return { event, envelope };
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ownerOf(event: Record<string, unknown>): Record<string, unknown> | null {
|
|
142
|
+
const p = event['payload'];
|
|
143
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) return null;
|
|
144
|
+
const owner = (p as Record<string, unknown>)['owner'];
|
|
145
|
+
return owner !== null && typeof owner === 'object' && !Array.isArray(owner) ? (owner as Record<string, unknown>) : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** One Ajv per wire: the v2 tree under `schemas/v2/`, the v1 tree at the root — each addressed by its absolute `$id`. */
|
|
149
|
+
function validators(major: 1 | 2): { ref: (id: string) => Validator } {
|
|
150
|
+
const a = new Ajv2020({ strict: false, allErrors: true }); addFormats(a);
|
|
151
|
+
const dir = major === 2 ? join(SCHEMAS_DIR, 'v2') : SCHEMAS_DIR;
|
|
152
|
+
for (const f of readdirSync(dir)) {
|
|
153
|
+
const full = join(dir, f);
|
|
154
|
+
if (!f.endsWith('.schema.json') || statSync(full).isDirectory()) continue;
|
|
155
|
+
try { a.addSchema(JSON.parse(readFileSync(full, 'utf8')) as Record<string, unknown>); } catch { /* duplicate $id */ }
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
ref: (id: string): Validator => {
|
|
159
|
+
const fn = a.compile({ $ref: id });
|
|
160
|
+
return (doc) => ({ ok: fn(doc) === true, errors: a.errorsText(fn.errors, { separator: '; ' }) });
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const V2 = 'https://openwop.dev/spec/v2/';
|
|
166
|
+
const V1 = 'https://openwop.dev/spec/v1/';
|
|
167
|
+
|
|
168
|
+
describe('webhook delivery shape is per-contract (webhooks.md §Delivery, versioning.md §1.2)', () => {
|
|
169
|
+
let active: Server | null = null;
|
|
170
|
+
afterEach(async () => { const s = active; active = null; if (s) await new Promise<void>((r) => s.close(() => r())); });
|
|
171
|
+
|
|
172
|
+
it('a major-2 subscriber receives the v2 rendering: the delivery validates, and run.started.owner carries subject, never principal', async () => {
|
|
173
|
+
if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
|
|
174
|
+
if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
|
|
175
|
+
const receiver = await startReceiver(); active = receiver.server;
|
|
176
|
+
if ((await register(receiver.url, 2)) === null) return softSkip('blocked', 'registration refused (reason recorded above)');
|
|
177
|
+
const runId = await driveRun();
|
|
178
|
+
const d = await waitFor(() => deliveryFor(receiver.deliveries, runId), 15_000);
|
|
179
|
+
if (!d) return softSkip('blocked', `no ${EVENT_TYPE} delivery for this run arrived inside 15s — durability is v2-webhook-durable-delivery's claim, not this file's`);
|
|
180
|
+
const v2 = validators(2);
|
|
181
|
+
const envelope = v2.ref(`${V2}webhook-delivery.schema.json`)(d.envelope);
|
|
182
|
+
expect(envelope.ok, req(ID, DOC, `a major-2 delivery MUST validate against webhook-delivery.schema.json (v2) — { runId, workspaceId?, event } with event the verbatim v2 run event. ${envelope.errors}`)).toBe(true);
|
|
183
|
+
const payload = v2.ref(`${V2}run-event-payloads.schema.json#/$defs/runStarted`)(d.event['payload']);
|
|
184
|
+
expect(payload.ok, req(ID, DOC, `a major-2 run.started payload MUST validate against the v2 runStarted definition — "one source, three renderings" (RFC 0171 §A.4). ${payload.errors}`)).toBe(true);
|
|
185
|
+
const owner = ownerOf(d.event);
|
|
186
|
+
expect(owner !== null, req(ID, DOC, 'a major-2 run.started payload MUST carry the owner echo { tenant, subject } (identity.md §1; run-event-payloads runStarted.owner requires both)')).toBe(true);
|
|
187
|
+
expect(Object.keys(owner ?? {}).filter((k) => k === 'principal' || k === 'principalKind'), req(ID, DOC, 'a major-2 owner echo MUST NOT carry the v1 keys principal / principalKind — that is the fan-out forwarding the in-process dialect instead of projecting')).toEqual([]);
|
|
188
|
+
expect(typeof owner?.['tenant'] === 'string' && owner?.['subject'] !== undefined, req(ID, DOC, 'a major-2 owner echo carries tenant and subject')).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('a major-1 subscriber still receives the v1 rendering — the v1 wire does not move mid-overlap', async () => {
|
|
192
|
+
if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
|
|
193
|
+
if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
|
|
194
|
+
const disc = await v2Discovery();
|
|
195
|
+
// RFC 0172 §A.1: `protocolVersions` lists every <major>.<minor> the host serves — the 2.3.1 file read a
|
|
196
|
+
// `versions.supported` that no discovery document has, so this leg was inapplicable on every host.
|
|
197
|
+
const versions = Array.isArray(disc?.['protocolVersions']) ? (disc?.['protocolVersions'] as unknown[]).map(String) : [];
|
|
198
|
+
if (!versions.some((v) => v.startsWith('1.'))) return softSkip('inapplicable', `host advertises [${versions.join(', ') || 'no protocolVersions'}] — no 1.x member, so there is no v1 wire to keep still`);
|
|
199
|
+
const receiver = await startReceiver(); active = receiver.server;
|
|
200
|
+
if ((await register(receiver.url, 1)) === null) return softSkip('blocked', 'registration refused or inapplicable (disposition recorded above)');
|
|
201
|
+
const runId = await driveRun();
|
|
202
|
+
const d = await waitFor(() => deliveryFor(receiver.deliveries, runId), 15_000);
|
|
203
|
+
if (!d) return softSkip('blocked', `no ${EVENT_TYPE} delivery for this run arrived inside 15s`);
|
|
204
|
+
// The v1 definition is the discriminator, not the owner's keys: v1's owner admits `subject` (RFC 0165
|
|
205
|
+
// §B, echoed verbatim when present) alongside `principal`, so a v2 owner is ALSO a valid v1 owner.
|
|
206
|
+
// What the v1 wire cannot carry is the v2 payload's integer `engineVersion` (string on v1) — a fan-out
|
|
207
|
+
// that forwards the v2 rendering to a 1.x subscriber fails here, on the field that actually differs.
|
|
208
|
+
const payload = validators(1).ref(`${V1}run-event-payloads.schema.json#/$defs/runStarted`)(d.event['payload']);
|
|
209
|
+
expect(payload.ok, req(ID, 'spec/v2/core/versioning.md §1.2', `a major-1 run.started payload MUST validate against the V1 runStarted definition — projecting every channel to v2 is as wrong as projecting none; the projection is contract-scoped. ${payload.errors}`)).toBe(true);
|
|
210
|
+
const owner = ownerOf(d.event);
|
|
211
|
+
expect(owner === null || typeof owner['tenant'] === 'string', req(ID, 'spec/v2/core/versioning.md §1.2', 'a v1 owner echo, when present, names the tenant (v1 runStarted.owner requires it)')).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('a seeded era-2 run is delivered projected too — the fan-out branch a fresh run cannot reach (seam-gated)', async () => {
|
|
215
|
+
const disc = await v2Discovery();
|
|
216
|
+
if (!disc) return softSkip('blocked', 'v2 discovery unreachable');
|
|
217
|
+
if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
|
|
218
|
+
// era2Gate answers null when the seam IS advertised and a disposition when it is not — the 2.3.1 file
|
|
219
|
+
// read null as absence, so this leg was inapplicable on exactly the hosts that could witness it.
|
|
220
|
+
const gate = era2Gate(disc);
|
|
221
|
+
if (gate !== null && !gate.ok) return softSkip(gate.kind, gate.reason);
|
|
222
|
+
const receiver = await startReceiver(); active = receiver.server;
|
|
223
|
+
if ((await register(receiver.url, 2)) === null) return softSkip('blocked', 'registration refused (reason recorded above)');
|
|
224
|
+
const log = await seedEra2Log(v1FixtureLog(FIXTURE), 'completed');
|
|
225
|
+
if (!log.ok) return softSkip(log.kind, log.reason);
|
|
226
|
+
const runId = log.runId;
|
|
227
|
+
const d = await waitFor(() => deliveryFor(receiver.deliveries, runId), 15_000);
|
|
228
|
+
// The seam appends HISTORY — rows that already happened — and a host MAY not fan out history (the
|
|
229
|
+
// reference host's seam appends with fan-out suppressed by design). No delivery inside 15s means the
|
|
230
|
+
// era-2 fan-out branch is unobservable on this host, not that a measurement failed: inapplicable.
|
|
231
|
+
if (!d) return softSkip('inapplicable', `no ${EVENT_TYPE} delivery for the seeded era-2 run inside 15s — this host does not fan out seeded history, so the era-2 fan-out branch is unobservable here (recorded under openwop.family.conformance)`);
|
|
232
|
+
const payload = validators(2).ref(`${V2}run-event-payloads.schema.json#/$defs/runStarted`)(d.event['payload']);
|
|
233
|
+
expect(payload.ok, req(ID, DOC, `an era-2 row delivered to a major-2 subscriber MUST be projected — the read projection applies at the fan-out as at poll/SSE (events.md §Era-2). ${payload.errors}`)).toBe(true);
|
|
234
|
+
const owner = ownerOf(d.event);
|
|
235
|
+
expect(owner === null || !('principal' in owner || 'principalKind' in owner), req(ID, DOC, 'a projected era-2 delivery MUST NOT carry the v1 owner keys')).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
});
|