@openwop/openwop-conformance 2.39.0 → 2.39.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 +11 -0
- package/README.md +1 -1
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +59 -27
- package/schemas/CORPUS-STAMP.json +9 -9
- package/src/lib/v2-projection.ts +34 -5
- package/src/scenarios/v2-a2a-operation-map.test.ts +21 -0
- package/src/scenarios/v2-a2a-push-delivery.test.ts +82 -37
- package/src/scenarios/v2-lane-issuer-advertised.test.ts +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.39.2] — 2026-09-25 — RFC 0208's unreadable-task leg records its own row
|
|
4
|
+
|
|
5
|
+
- **`0208.a2a-unreadable-not-found` records its own row.** In `v2-a2a-operation-map`, one `it()` cited both RFC 0208's `a2a-unreadable-not-found` and RFC 0211's `a2a-unreadable-not-found-details`. The per-`it` ledger keeps only the last id, so the 0208 row never reached a bundle: it is absent from the certified 2.38.0 v2-reference cut, and it is the one row keeping RFC 0208 from `Accepted` (the other 18 pass). This is the same class as openwop#1552. RFC 0211's detail comparison now has its own `it()` with its own setup, so each id records once. `scripts/check-req-only.mjs` already failed an `it()` citing two LITERAL ids, but it read helper-built ids (`R('x')`, `R11('x')`) as no id at all, which is how this one passed. It now counts each (helper, slug) as an id, fails against the 2.39.1 scenario, and finds no other case in 633 scenario files.
|
|
6
|
+
- **`src/lib/v2-projection.ts`: `stripSupported` is now `stripSupportedFlag`, named for the one thing it does** (TODO S1). It removes the retired `supported` flag and nothing else. The corpus generator's function of the same old name (now `projectV1FacetSchema`) also drops `tier`/`experimentalUntil`, folds `supported`-gated if/then into `required`, closes objects and rewrites prose. Under one name, a host reading either assumed the other matched. Each now documents the other. **`stripSupported` stays as a `@deprecated` alias**, because openwop-app's `whd7-v2-projection-parity.test.ts` imports it by that name; it is removed at the next suite major. `carriesUnspliceablePayload` is unchanged, and a self-test now holds it equal to the generator's copy (`scripts/v2-unspliceable.mjs`) on every node of the v1 capabilities schema.
|
|
7
|
+
- **Version moved ahead of publication.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.39.2` because `2.39.1` is tagged and publishing. Not tagged, not published.
|
|
8
|
+
|
|
9
|
+
## [2.39.1] — 2026-09-25 — the RFC 0214 delivery legs survive a tunnel, and each one leaves its own row
|
|
10
|
+
|
|
11
|
+
- **`v2-a2a-push-delivery` — every requirement now records a row, and the budgets fit a public front.** The 2.39.0 public push-on cut of the v2 reference host lost three legs (§A after-Delete, §B 3xx redirect, §D replay fork) to vitest's per-test timeout, not to an assertion — deliveries were arriving through the front (`no-openwop-signature` passed). Budgets now scale when `OPENWOP_WEBHOOK_RECEIVER_URL` is wired (arrival 60 s, settle 45 s, leg 240 s, fork leg 300 s; loopback unchanged), the #1537 precedent. Separately, `0214.a2a-push-delivery-authenticated` vanished from that bundle because its `it()` also cited `0214.a2a-push-no-openwop-signature` and the ledger keeps the LAST id an `it()` cites: the delivery is now made once and read by two legs, one id each, and when the delivery cannot be made the signature leg records `blocked` with the reason instead of disappearing.
|
|
12
|
+
- **`v2-lane-issuer-advertised` no longer fails a host whose anonymous lane omits `revocation`, as the schema and `identity.md` §2.2 allow.** openwop#1540 (2.38.0) made `revocation` optional on the `anonymous` lane, whose §2.2 row reads "—", but this scenario's `.members` leg still asserted `revocation` on every lane. A host that followed the schema and the prose therefore failed the published suite; openwop-app-ce measured it bumping openwop-app's pin. A "—" lane may now omit `revocation`; if it advertises one, it must still be a known member, so no new failure mode is added. `scripts/check-lane-revocation-rules.mjs` gains a fifth assertion, that the `.members` leg exempts the "—" lanes, which fails against the 2.38.0/2.39.0 scenario.
|
|
13
|
+
|
|
3
14
|
## [2.39.0] — 2026-09-24 — an A2A push is measured at a receiver the suite owns
|
|
4
15
|
|
|
5
16
|
- **RFC 0214 — the push legs land, and a host that advertises `a2a.pushNotifications` is now measured, not just the one that refuses.** `v2-a2a-operation-map` gains three legs gated on `pushNotifications: true`: `0214.a2a-push-register-ssrf` (a non-https, private, loopback or link-local destination is not registered), `0214.a2a-push-config-isolation` (a config read under another tenant's task, another of the caller's tasks, or an unknown id is answered byte-identically; a foreign delete deletes nothing; delete is idempotent) and `0214.a2a-push-secrets-not-returned` (the registered token and credentials are absent from Create/Get/List, `GetTask`, the run snapshot and the event log). New scenario `v2-a2a-push-delivery` measures delivery at a SUITE-OWNED scoped receiver behind the operator's public front (the RFC 0158 receiver): `0214.a2a-push-delivery-authenticated` (§A/§C — at least one POST, `Authorization: {scheme} {credentials}`, `application/a2a+json`, a `StreamResponse` naming the task), `0214.a2a-push-no-openwop-signature`, `0214.a2a-push-no-redirect` (§B — a 307 target receives nothing), `0214.a2a-push-discard-on-delete` (§A, with a live-config positive control) and `0214.a2a-push-fork-no-push` (§D). Every leg suspends its task at INPUT_REQUIRED BEFORE registering, so none can pass on a push nobody owed; without a public https front the legs record `blocked` (a conforming host refuses a loopback destination), never a pass and never a relaxed guard. Stated limit: the delivery-time DNS re-resolve arm is host-unit-tested; the suite does not control the host's resolver.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
|
|
12
12
|
# default resolver refuses. npm 10.9 fails outright with
|
|
13
13
|
# "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
|
|
14
|
-
npm install --legacy-peer-deps @openwop/openwop-conformance@2.39.
|
|
14
|
+
npm install --legacy-peer-deps @openwop/openwop-conformance@2.39.2 @openwop/spec-artifacts@2.39.2
|
|
15
15
|
# or run without install:
|
|
16
16
|
npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
|
|
17
17
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.39.
|
|
3
|
+
"version": "2.39.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.39.
|
|
59
|
+
"@openwop/spec-artifacts": "2.39.2"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
5
|
"files": 633,
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
6
|
+
"tests": 2424,
|
|
7
|
+
"withStableId": 2424,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
9
|
"explicitIds": 2304
|
|
10
10
|
},
|
|
@@ -26690,6 +26690,30 @@
|
|
|
26690
26690
|
"line": 180,
|
|
26691
26691
|
"title": "an unknown task and another tenant's task are answered by the same TaskNotFoundError",
|
|
26692
26692
|
"explicitId": null,
|
|
26693
|
+
"citations": [
|
|
26694
|
+
{
|
|
26695
|
+
"section": "identity.md §5; a2a.operations GetTask",
|
|
26696
|
+
"requirement": null,
|
|
26697
|
+
"interpolated": true
|
|
26698
|
+
},
|
|
26699
|
+
{
|
|
26700
|
+
"section": "a2a.errors TaskNotFoundError",
|
|
26701
|
+
"requirement": null,
|
|
26702
|
+
"interpolated": true
|
|
26703
|
+
},
|
|
26704
|
+
{
|
|
26705
|
+
"section": "interop.md §\"The operation mappings\" Isolation",
|
|
26706
|
+
"requirement": null,
|
|
26707
|
+
"interpolated": true
|
|
26708
|
+
}
|
|
26709
|
+
]
|
|
26710
|
+
},
|
|
26711
|
+
{
|
|
26712
|
+
"id": "openwop.it.v2-a2a-operation-map.an-unknown-task-and-another-tenant-s-task-carry-equivalent-errorinfo-details-rfc",
|
|
26713
|
+
"file": "v2-a2a-operation-map.test.ts",
|
|
26714
|
+
"line": 201,
|
|
26715
|
+
"title": "an unknown task and another tenant's task carry equivalent ErrorInfo details (RFC 0211 §D)",
|
|
26716
|
+
"explicitId": null,
|
|
26693
26717
|
"citations": [
|
|
26694
26718
|
{
|
|
26695
26719
|
"section": "identity.md §5; a2a.operations GetTask",
|
|
@@ -26720,7 +26744,7 @@
|
|
|
26720
26744
|
{
|
|
26721
26745
|
"id": "openwop.it.v2-a2a-operation-map.task-status-state-is-the-map-s-projection-of-the-run-status-at-every-observed-st",
|
|
26722
26746
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26723
|
-
"line":
|
|
26747
|
+
"line": 223,
|
|
26724
26748
|
"title": "Task.status.state is the map's projection of the run status at every observed state",
|
|
26725
26749
|
"explicitId": null,
|
|
26726
26750
|
"citations": [
|
|
@@ -26743,7 +26767,7 @@
|
|
|
26743
26767
|
{
|
|
26744
26768
|
"id": "openwop.it.v2-a2a-operation-map.the-json-rpc-error-codes-the-suite-can-cause-are-the-a2a-1-0-1-codes",
|
|
26745
26769
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26746
|
-
"line":
|
|
26770
|
+
"line": 248,
|
|
26747
26771
|
"title": "the JSON-RPC error codes the suite can cause are the A2A 1.0.1 codes",
|
|
26748
26772
|
"explicitId": null,
|
|
26749
26773
|
"citations": [
|
|
@@ -26766,7 +26790,7 @@
|
|
|
26766
26790
|
{
|
|
26767
26791
|
"id": "openwop.it.v2-a2a-operation-map.a-row-whose-requires-facet-is-not-advertised-is-refused-with-the-row-s-error",
|
|
26768
26792
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26769
|
-
"line":
|
|
26793
|
+
"line": 262,
|
|
26770
26794
|
"title": "a row whose requires facet is not advertised is refused with the row's error",
|
|
26771
26795
|
"explicitId": null,
|
|
26772
26796
|
"citations": [
|
|
@@ -26784,7 +26808,7 @@
|
|
|
26784
26808
|
{
|
|
26785
26809
|
"id": "openwop.it.v2-a2a-operation-map.a-host-not-advertising-a2a-pushnotifications-refuses-all-four-push-config-operat",
|
|
26786
26810
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26787
|
-
"line":
|
|
26811
|
+
"line": 275,
|
|
26788
26812
|
"title": "a host not advertising a2a.pushNotifications refuses all four push-config operations with PushNotificationNotSupportedError",
|
|
26789
26813
|
"explicitId": null,
|
|
26790
26814
|
"citations": [
|
|
@@ -26808,7 +26832,7 @@
|
|
|
26808
26832
|
{
|
|
26809
26833
|
"id": "openwop.it.v2-a2a-operation-map.rfc-0214-createtaskpushnotificationconfig-refuses-a-non-https-or-private-destina",
|
|
26810
26834
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26811
|
-
"line":
|
|
26835
|
+
"line": 306,
|
|
26812
26836
|
"title": "RFC 0214 — CreateTaskPushNotificationConfig refuses a non-https or private destination",
|
|
26813
26837
|
"explicitId": null,
|
|
26814
26838
|
"citations": [
|
|
@@ -26832,7 +26856,7 @@
|
|
|
26832
26856
|
{
|
|
26833
26857
|
"id": "openwop.it.v2-a2a-operation-map.rfc-0214-e-a-push-config-of-another-tenant-s-task-of-another-task-or-unknown-is",
|
|
26834
26858
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26835
|
-
"line":
|
|
26859
|
+
"line": 324,
|
|
26836
26860
|
"title": "RFC 0214 §E — a push config of another tenant's task, of another task, or unknown, is answered identically",
|
|
26837
26861
|
"explicitId": null,
|
|
26838
26862
|
"citations": [
|
|
@@ -26880,7 +26904,7 @@
|
|
|
26880
26904
|
{
|
|
26881
26905
|
"id": "openwop.it.v2-a2a-operation-map.rfc-0214-a-a-push-config-s-token-and-credentials-are-never-returned-and-never-re",
|
|
26882
26906
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26883
|
-
"line":
|
|
26907
|
+
"line": 368,
|
|
26884
26908
|
"title": "RFC 0214 §A — a push config's token and credentials are never returned, and never reach task, events or state",
|
|
26885
26909
|
"explicitId": null,
|
|
26886
26910
|
"citations": [
|
|
@@ -26904,7 +26928,7 @@
|
|
|
26904
26928
|
{
|
|
26905
26929
|
"id": "openwop.it.v2-a2a-operation-map.listtasks-returns-only-what-listruns-returns-to-the-same-subject-tenant-and-cont",
|
|
26906
26930
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26907
|
-
"line":
|
|
26931
|
+
"line": 402,
|
|
26908
26932
|
"title": "ListTasks returns only what listRuns returns to the same Subject; tenant and contextId never select",
|
|
26909
26933
|
"explicitId": null,
|
|
26910
26934
|
"citations": [
|
|
@@ -26941,7 +26965,7 @@
|
|
|
26941
26965
|
{
|
|
26942
26966
|
"id": "openwop.it.v2-a2a-operation-map.an-a2a-error-s-data-is-an-array-carrying-one-errorinfo-with-the-map-row-s-reason",
|
|
26943
26967
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26944
|
-
"line":
|
|
26968
|
+
"line": 435,
|
|
26945
26969
|
"title": "an A2A error's data is an array carrying one ErrorInfo with the map row's reason and the A2A domain",
|
|
26946
26970
|
"explicitId": null,
|
|
26947
26971
|
"citations": [
|
|
@@ -26975,7 +26999,7 @@
|
|
|
26975
26999
|
{
|
|
26976
27000
|
"id": "openwop.it.v2-a2a-operation-map.no-response-on-the-card-s-json-rpc-interface-url-is-the-openwop-error-envelope",
|
|
26977
27001
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26978
|
-
"line":
|
|
27002
|
+
"line": 456,
|
|
26979
27003
|
"title": "no response on the card's JSON-RPC interface URL is the OpenWOP error envelope",
|
|
26980
27004
|
"explicitId": null,
|
|
26981
27005
|
"citations": [
|
|
@@ -26993,7 +27017,7 @@
|
|
|
26993
27017
|
{
|
|
26994
27018
|
"id": "openwop.it.v2-a2a-operation-map.versionnotsupportederror-carries-errorinfo-version-not-supported-and-supportedve",
|
|
26995
27019
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26996
|
-
"line":
|
|
27020
|
+
"line": 476,
|
|
26997
27021
|
"title": "VersionNotSupportedError carries ErrorInfo VERSION_NOT_SUPPORTED, and supportedVersions is a comma-joined subset of the card",
|
|
26998
27022
|
"explicitId": null,
|
|
26999
27023
|
"citations": [
|
|
@@ -27017,7 +27041,7 @@
|
|
|
27017
27041
|
{
|
|
27018
27042
|
"id": "openwop.it.v2-a2a-operation-map.an-http-json-interface-answers-an-a2a-error-with-google-rpc-status-carrying-the",
|
|
27019
27043
|
"file": "v2-a2a-operation-map.test.ts",
|
|
27020
|
-
"line":
|
|
27044
|
+
"line": 494,
|
|
27021
27045
|
"title": "an HTTP+JSON interface answers an A2A error with google.rpc.Status carrying the ErrorInfo",
|
|
27022
27046
|
"explicitId": null,
|
|
27023
27047
|
"citations": [
|
|
@@ -27036,7 +27060,7 @@
|
|
|
27036
27060
|
{
|
|
27037
27061
|
"id": "openwop.it.v2-a2a-operation-map.a-run-suspended-on-a-credential-interrupt-projects-to-task-state-auth-required-n",
|
|
27038
27062
|
"file": "v2-a2a-operation-map.test.ts",
|
|
27039
|
-
"line":
|
|
27063
|
+
"line": 509,
|
|
27040
27064
|
"title": "a run suspended on a credential interrupt projects to TASK_STATE_AUTH_REQUIRED, naming the provider and carrying connectUrl",
|
|
27041
27065
|
"explicitId": null,
|
|
27042
27066
|
"citations": [
|
|
@@ -27072,15 +27096,10 @@
|
|
|
27072
27096
|
{
|
|
27073
27097
|
"id": "openwop.it.v2-a2a-push-delivery.a-c-a-transition-after-registration-is-posted-to-the-registered-destination-with",
|
|
27074
27098
|
"file": "v2-a2a-push-delivery.test.ts",
|
|
27075
|
-
"line":
|
|
27076
|
-
"title": "§A/§C — a transition after registration is POSTed to the registered destination with the registered Authorization
|
|
27099
|
+
"line": 189,
|
|
27100
|
+
"title": "§A/§C — a transition after registration is POSTed to the registered destination with the registered Authorization",
|
|
27077
27101
|
"explicitId": null,
|
|
27078
27102
|
"citations": [
|
|
27079
|
-
{
|
|
27080
|
-
"section": "spec/v2/core/interop.md §\"A2A push delivery\" (RFC 0214)",
|
|
27081
|
-
"requirement": null,
|
|
27082
|
-
"interpolated": true
|
|
27083
|
-
},
|
|
27084
27103
|
{
|
|
27085
27104
|
"section": null,
|
|
27086
27105
|
"requirement": null,
|
|
@@ -27103,6 +27122,19 @@
|
|
|
27103
27122
|
"section": "A2A v1.0.1 §4.3.3 (StreamResponse)",
|
|
27104
27123
|
"requirement": null,
|
|
27105
27124
|
"interpolated": true
|
|
27125
|
+
}
|
|
27126
|
+
]
|
|
27127
|
+
},
|
|
27128
|
+
{
|
|
27129
|
+
"id": "openwop.it.v2-a2a-push-delivery.c-an-a2a-push-carries-no-openwop-signature",
|
|
27130
|
+
"file": "v2-a2a-push-delivery.test.ts",
|
|
27131
|
+
"line": 203,
|
|
27132
|
+
"title": "§C — an A2A push carries no OpenWOP signature",
|
|
27133
|
+
"explicitId": null,
|
|
27134
|
+
"citations": [
|
|
27135
|
+
{
|
|
27136
|
+
"section": "spec/v2/core/interop.md §\"A2A push delivery\" (RFC 0214)",
|
|
27137
|
+
"requirement": "positive control: at least one push arrived to read headers from"
|
|
27106
27138
|
},
|
|
27107
27139
|
{
|
|
27108
27140
|
"section": null,
|
|
@@ -27114,7 +27146,7 @@
|
|
|
27114
27146
|
{
|
|
27115
27147
|
"id": "openwop.it.v2-a2a-push-delivery.b-a-3xx-from-the-destination-is-a-failed-delivery-the-redirect-target-receives-n",
|
|
27116
27148
|
"file": "v2-a2a-push-delivery.test.ts",
|
|
27117
|
-
"line":
|
|
27149
|
+
"line": 215,
|
|
27118
27150
|
"title": "§B — a 3xx from the destination is a failed delivery: the redirect target receives nothing, credential or not",
|
|
27119
27151
|
"explicitId": null,
|
|
27120
27152
|
"citations": [
|
|
@@ -27138,7 +27170,7 @@
|
|
|
27138
27170
|
{
|
|
27139
27171
|
"id": "openwop.it.v2-a2a-push-delivery.a-after-delete-no-further-push-reaches-that-destination-while-a-live-config-on-t",
|
|
27140
27172
|
"file": "v2-a2a-push-delivery.test.ts",
|
|
27141
|
-
"line":
|
|
27173
|
+
"line": 237,
|
|
27142
27174
|
"title": "§A — after Delete no further push reaches that destination, while a live config on the same task still receives",
|
|
27143
27175
|
"explicitId": null,
|
|
27144
27176
|
"citations": [
|
|
@@ -27167,7 +27199,7 @@
|
|
|
27167
27199
|
{
|
|
27168
27200
|
"id": "openwop.it.v2-a2a-push-delivery.d-a-replay-fork-of-a-pushed-run-delivers-nothing-to-the-source-run-s-destination",
|
|
27169
27201
|
"file": "v2-a2a-push-delivery.test.ts",
|
|
27170
|
-
"line":
|
|
27202
|
+
"line": 263,
|
|
27171
27203
|
"title": "§D — a replay fork of a pushed run delivers nothing to the source run's destination",
|
|
27172
27204
|
"explicitId": null,
|
|
27173
27205
|
"citations": [
|
|
@@ -29648,7 +29680,7 @@
|
|
|
29648
29680
|
{
|
|
29649
29681
|
"id": "openwop.it.v2-lane-issuer-advertised.a-windowed-revocation-rule-advertises-revocationwindowseconds",
|
|
29650
29682
|
"file": "v2-lane-issuer-advertised.test.ts",
|
|
29651
|
-
"line":
|
|
29683
|
+
"line": 93,
|
|
29652
29684
|
"title": "a windowed revocation rule advertises revocationWindowSeconds",
|
|
29653
29685
|
"explicitId": "openwop.requirement.0170.lane-issuer-advertised.window",
|
|
29654
29686
|
"citations": [
|
|
@@ -29662,7 +29694,7 @@
|
|
|
29662
29694
|
{
|
|
29663
29695
|
"id": "openwop.it.v2-lane-issuer-advertised.a-lane-advertises-only-a-revocation-rule-its-identity-md-2-2-row-lists",
|
|
29664
29696
|
"file": "v2-lane-issuer-advertised.test.ts",
|
|
29665
|
-
"line":
|
|
29697
|
+
"line": 104,
|
|
29666
29698
|
"title": "a lane advertises only a revocation rule its identity.md §2.2 row lists",
|
|
29667
29699
|
"explicitId": "openwop.requirement.0170.lane-issuer-advertised.lane-rule",
|
|
29668
29700
|
"citations": [
|
|
@@ -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.39.
|
|
5
|
-
"corpusTag": "v2.39.
|
|
4
|
+
"version": "2.39.2",
|
|
5
|
+
"corpusTag": "v2.39.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": "6366b5514be71098e9ea41fc81ff52bc39db1a547b353c0b344cc2a2bf5370aa",
|
|
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": "bac1af1953801e2e3e80cf3aa006b168611cb5b20ccb70c52674815437204e85",
|
|
13
|
+
"api/v2/asyncapi.yaml": "77f9311485f7220c28fb81937670ba415b9a3edc0adf73ad3c77e1e288247548",
|
|
14
|
+
"api/v2/openapi.yaml": "5bf2dd1dfd7762e0c108ffe51367933108d701d8dd8549aa6e21f1cbece7291b",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
205
205
|
"spec/v1/alias-detectors.json": "2401fcb1c18cdd688c018b3d716ae6bca85c5e356220ee2b793bd9d81872412d",
|
|
206
206
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
207
|
-
"spec/v1/core-standard-manifest.json": "
|
|
207
|
+
"spec/v1/core-standard-manifest.json": "4b144e92eae7afe2e541888d89c5461853f0cf1b929e9dca6a253fbe78d3e591",
|
|
208
208
|
"spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
|
|
209
209
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
210
210
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -292,9 +292,9 @@
|
|
|
292
292
|
"spec/v2/path-manifest.json": "ae9b56d03a701061fd48ad0b73cb0373493f6a4547da92763b422295b5e19535",
|
|
293
293
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
294
294
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
295
|
-
"spec/v2/release.json": "
|
|
295
|
+
"spec/v2/release.json": "00fba2903eb7f34d8c98a7cd11fee02590f5390f3109e5bfdba85f9ceeeddf83",
|
|
296
296
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
297
|
-
"spec/v2/surface-baseline.json": "
|
|
297
|
+
"spec/v2/surface-baseline.json": "314e72a7687918b11a89875d0b735ea4ec1589bd6ccb8abcf574d5e2fbba5333"
|
|
298
298
|
},
|
|
299
|
-
"corpusCommit": "
|
|
299
|
+
"corpusCommit": "57cc8d88f96abb980c605cc386167861b227ff97"
|
|
300
300
|
}
|
package/src/lib/v2-projection.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Remove the retired `supported` flag from a v1 capability schema fragment
|
|
3
|
+
* (or an advertised value), and flag payload a v2 record cannot splice.
|
|
3
4
|
*
|
|
4
5
|
* WHY THIS EXISTS, in three measured instances from one week:
|
|
5
6
|
*
|
|
@@ -24,17 +25,37 @@
|
|
|
24
25
|
* It deliberately does NOT invent shape. If a v1 value carries payload a v2
|
|
25
26
|
* record cannot hold, that is RFC 0193's named-seat problem and needs a person
|
|
26
27
|
* to name the seat; this function will not paper over it.
|
|
28
|
+
*
|
|
29
|
+
* ── What it is NOT ──────────────────────────────────────────────────────────
|
|
30
|
+
* It is not the corpus's v2 facet projection. `projectV1FacetSchema` in
|
|
31
|
+
* scripts/generate-from-declaration.mjs builds schemas/v2/capabilities.schema.json
|
|
32
|
+
* and does four more things this does not:
|
|
33
|
+
*
|
|
34
|
+
* - drops `tier` and `experimentalUntil` (the record's `status`/`until`
|
|
35
|
+
* absorb them — mapping a v1 tier onto a v2 status is a decision, not a strip);
|
|
36
|
+
* - folds `supported`-gated if/then into unconditional `required` (RFC 0192 §A);
|
|
37
|
+
* here the conditional stays, its `if` reduced to an always-true
|
|
38
|
+
* `{ properties: {} }` — the same verdict, but not the same schema;
|
|
39
|
+
* - closes objects (`additionalProperties: false`);
|
|
40
|
+
* - rewrites `supported` prose in descriptions (RFC 0192 §B).
|
|
41
|
+
*
|
|
42
|
+
* So `multiAgent.executionModel` passed through this still carries `tier` and
|
|
43
|
+
* `experimentalUntil`. Both functions were named `stripSupported` until suite
|
|
44
|
+
* 2.38.x, which hid that difference (the old name survives as a deprecated
|
|
45
|
+
* alias for openwop-app's parity test); do not merge them — they have different
|
|
46
|
+
* contracts. `carriesUnspliceablePayload` IS the same predicate as
|
|
47
|
+
* scripts/v2-unspliceable.mjs, and a self-test holds the two copies equal.
|
|
27
48
|
*/
|
|
28
49
|
|
|
29
|
-
/** Recursively drop `supported` — at every depth, and out of any `required[]`. */
|
|
30
|
-
export function
|
|
31
|
-
if (Array.isArray(value)) return value.map((v) =>
|
|
50
|
+
/** Recursively drop `supported` — at every depth, and out of any `required[]`. Nothing else. */
|
|
51
|
+
export function stripSupportedFlag<T>(value: T): T {
|
|
52
|
+
if (Array.isArray(value)) return value.map((v) => stripSupportedFlag(v)) as unknown as T;
|
|
32
53
|
if (value === null || typeof value !== 'object') return value;
|
|
33
54
|
|
|
34
55
|
const out: Record<string, unknown> = {};
|
|
35
56
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
36
57
|
if (k === 'supported') continue;
|
|
37
|
-
out[k] =
|
|
58
|
+
out[k] = stripSupportedFlag(v);
|
|
38
59
|
}
|
|
39
60
|
// A `required` list naming `supported` keeps a retired field mandatory, which
|
|
40
61
|
// is how a closed v2 record ends up unsatisfiable by any honest host.
|
|
@@ -46,6 +67,14 @@ export function stripSupported<T>(value: T): T {
|
|
|
46
67
|
return out as T;
|
|
47
68
|
}
|
|
48
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @deprecated Renamed `stripSupportedFlag` in suite 2.38.x; the old name was shared
|
|
72
|
+
* with the generator's full schema projection and hid the difference. Kept because
|
|
73
|
+
* openwop-app's `backend/typescript/test/whd7-v2-projection-parity.test.ts` imports
|
|
74
|
+
* it by this name. Remove at the next suite major.
|
|
75
|
+
*/
|
|
76
|
+
export const stripSupported = stripSupportedFlag;
|
|
77
|
+
|
|
49
78
|
/**
|
|
50
79
|
* True when a v1 value carries payload a uniform v2 record cannot splice —
|
|
51
80
|
* an array, a map, an enum or a scalar rather than an object with properties.
|
|
@@ -190,6 +190,27 @@ describe('RFC 0208 — v2-a2a-operation-map (host as A2A 1.0 server, gated on a2
|
|
|
190
190
|
const b = await rpc(t.url, 'GetTask', { id: foreign });
|
|
191
191
|
expect(a.error?.code, req(R('a2a-unreadable-not-found'), 'a2a.errors TaskNotFoundError', `an unknown task MUST be TaskNotFoundError -32001 (got ${JSON.stringify(a.error ?? a.result)})`)).toBe(-32001);
|
|
192
192
|
expect(b.error?.code, req(R('a2a-unreadable-not-found'), 'interop.md §"The operation mappings" Isolation', `a task the caller cannot read — here a foreign tenant segment REST refuses 403 — MUST be answered exactly as a nonexistent one, -32001 (got ${JSON.stringify(b.error ?? b.result)}, HTTP ${b.status})`)).toBe(-32001);
|
|
193
|
+
await rpc(t.url, 'CancelTask', { id });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// RFC 0211's detail comparison gets its OWN it(): an it() that cites two ids
|
|
197
|
+
// records only the LAST one in the ledger, and until 2.39.2 this setup lived in
|
|
198
|
+
// one it() with 0208's assertions — so `0208.a2a-unreadable-not-found` never
|
|
199
|
+
// reached a bundle (absent from the certified 2.38.0 v2-reference cut, the only
|
|
200
|
+
// row keeping RFC 0208 from Accepted). Same class as openwop#1552.
|
|
201
|
+
it('an unknown task and another tenant\'s task carry equivalent ErrorInfo details (RFC 0211 §D)', async () => {
|
|
202
|
+
const t = await target(true);
|
|
203
|
+
if (!t.ok) return skip(t, R11('a2a-unreadable-not-found-details'));
|
|
204
|
+
const first = await startApprovalTask(t.url);
|
|
205
|
+
const id = first.task?.id;
|
|
206
|
+
expect(typeof id === 'string' && id.includes('/'), req(R11('a2a-unreadable-not-found-details'), 'identity.md §5; a2a.operations GetTask', `Task.id is the tenant-bound runId <tenant>/<opaque> (got ${JSON.stringify(id)})`)).toBe(true);
|
|
207
|
+
const [tenant, opaque] = [id!.slice(0, id!.indexOf('/')), id!.slice(id!.indexOf('/') + 1)];
|
|
208
|
+
const fabricated = `${tenant}/${randomBytes(16).toString('base64url').replace(/[^A-Za-z0-9]/g, 'x').slice(0, 22)}`;
|
|
209
|
+
const foreign = `zz-conformance-foreign/${opaque}`;
|
|
210
|
+
const a = await rpc(t.url, 'GetTask', { id: fabricated });
|
|
211
|
+
const b = await rpc(t.url, 'GetTask', { id: foreign });
|
|
212
|
+
expect(a.error?.code, req(R11('a2a-unreadable-not-found-details'), 'a2a.errors TaskNotFoundError', `an unknown task MUST be TaskNotFoundError -32001 (got ${JSON.stringify(a.error ?? a.result)})`)).toBe(-32001);
|
|
213
|
+
expect(b.error?.code, req(R11('a2a-unreadable-not-found-details'), 'interop.md §"The operation mappings" Isolation', `a task the caller cannot read — here a foreign tenant segment REST refuses 403 — MUST be answered exactly as a nonexistent one, -32001 (got ${JSON.stringify(b.error ?? b.result)}, HTTP ${b.status})`)).toBe(-32001);
|
|
193
214
|
// RFC 0211 §D: compare the details per element. `Object.keys(data)` of a one-element
|
|
194
215
|
// array is ["0"] whatever the element discloses, so a key comparison is vacuous.
|
|
195
216
|
const na = normaliseErrorData(a.error?.data, fabricated);
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* @see spec/v2/core/interop.md §"A2A push delivery"
|
|
31
31
|
* @see spec/v2/core/security-defaults.md §"Onward hops"
|
|
32
32
|
*/
|
|
33
|
-
import { describe, it, expect } from 'vitest';
|
|
33
|
+
import { afterAll, describe, it, expect } from 'vitest';
|
|
34
34
|
import { randomBytes } from 'node:crypto';
|
|
35
35
|
import type { ServerResponse } from 'node:http';
|
|
36
36
|
import { driver } from '../lib/driver.js';
|
|
@@ -103,13 +103,26 @@ async function until(pred: () => boolean, ms: number): Promise<boolean> {
|
|
|
103
103
|
return pred();
|
|
104
104
|
}
|
|
105
105
|
const wait = (ms: number): Promise<void> => new Promise((ok) => setTimeout(ok, ms));
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Budgets. On a public cut every step crosses the operator's tunnel (suite → host
|
|
109
|
+
* front, host → receiver front), and the 2.39.0 public cut lost three legs to
|
|
110
|
+
* vitest's per-test timeout — not to an assertion — while deliveries were in
|
|
111
|
+
* fact arriving (#1537 precedent: the RFC 0199 state leg). Loopback keeps the
|
|
112
|
+
* tight budgets; a fronted run gets room for the round trips.
|
|
113
|
+
*/
|
|
114
|
+
const FRONTED = Boolean(process.env.OPENWOP_WEBHOOK_RECEIVER_URL);
|
|
115
|
+
const ARRIVAL_MS = FRONTED ? 60_000 : 20_000;
|
|
116
|
+
const SETTLE_MS = FRONTED ? 45_000 : 10_000;
|
|
117
|
+
const LEG_MS = FRONTED ? 240_000 : 60_000;
|
|
118
|
+
const FORK_LEG_MS = FRONTED ? 300_000 : 90_000;
|
|
106
119
|
const header = (h: ScopedHit, name: string): string | undefined => { const v = h.headers[name.toLowerCase()]; return Array.isArray(v) ? v.join(',') : v; };
|
|
107
120
|
|
|
108
121
|
async function runStatus(taskId: string): Promise<string | undefined> {
|
|
109
122
|
const r = await driver.get(`/runs/${encodeURIComponent(taskId)}`);
|
|
110
123
|
return r.status === 200 ? ((r.json as { status?: string }).status) : undefined;
|
|
111
124
|
}
|
|
112
|
-
async function settle(taskId: string, want: (s: string | undefined) => boolean, ms =
|
|
125
|
+
async function settle(taskId: string, want: (s: string | undefined) => boolean, ms = SETTLE_MS): Promise<string | undefined> {
|
|
113
126
|
const end = Date.now() + ms;
|
|
114
127
|
let s = await runStatus(taskId);
|
|
115
128
|
while (!want(s) && Date.now() < end) { await wait(150); s = await runStatus(taskId); }
|
|
@@ -136,36 +149,68 @@ function unmeasurable(rx: ScopedReceiver, created: Rpc): string | null {
|
|
|
136
149
|
return null;
|
|
137
150
|
}
|
|
138
151
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
/**
|
|
153
|
+
* One delivery, read by two legs. Each leg cites exactly one requirement id —
|
|
154
|
+
* the ledger keeps the LAST id an it() cites, so the 2.39.0 cut, whose single
|
|
155
|
+
* §A/§C it() cited both, recorded `no-openwop-signature` and silently dropped
|
|
156
|
+
* `delivery-authenticated`. The delivery runs once (lazily, in the first leg)
|
|
157
|
+
* and the second leg reads the same hits; when the delivery cannot be made the
|
|
158
|
+
* second leg records `blocked` with the reason instead of vanishing.
|
|
159
|
+
*/
|
|
160
|
+
type Delivery =
|
|
161
|
+
| { kind: 'measured'; hits: ScopedHit[]; taskId: string; cred: string; rx: ScopedReceiver }
|
|
162
|
+
| { kind: 'skip'; skip: 'inapplicable' | 'blocked'; reason: string };
|
|
163
|
+
let delivery: Promise<Delivery> | undefined;
|
|
164
|
+
let deliveryRx: ScopedReceiver | undefined;
|
|
165
|
+
function deliverOnce(id: string = R('a2a-push-delivery-authenticated')): Promise<Delivery> {
|
|
166
|
+
delivery ??= (async (): Promise<Delivery> => {
|
|
142
167
|
const g = await gate();
|
|
143
|
-
if (!g.ok) return
|
|
168
|
+
if (!g.ok) return { kind: 'skip', skip: g.kind, reason: g.reason };
|
|
144
169
|
const { rx, hits } = await receiver();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
deliveryRx = rx;
|
|
171
|
+
const taskId = await suspendedTask(g.url, id);
|
|
172
|
+
const cred = `owpush${randomBytes(12).toString('hex')}`;
|
|
173
|
+
const created = await create(g.url, taskId, rx.url, { authentication: { scheme: 'Bearer', credentials: cred } });
|
|
174
|
+
const why = unmeasurable(rx, created);
|
|
175
|
+
if (why !== null) { await rpc(g.url, 'CancelTask', { id: taskId }); return { kind: 'skip', skip: 'blocked', reason: why }; }
|
|
176
|
+
expect(created.error, req(id, DOC, `Create on a suspended task with the public receiver MUST succeed (got ${JSON.stringify(created.error)})`)).toBeUndefined();
|
|
177
|
+
await rpc(g.url, 'SendMessage', { message: acceptMessage(taskId) });
|
|
178
|
+
await settle(taskId, (s) => s !== undefined && TERMINAL.has(s));
|
|
179
|
+
const arrived = await until(() => hits.length > 0, ARRIVAL_MS);
|
|
180
|
+
if (!arrived && absenceIsUnmeasured(rx)) return { kind: 'skip', skip: 'blocked', reason: noDeliveryCause(rx, 'A2A push') };
|
|
181
|
+
return { kind: 'measured', hits, taskId, cred, rx };
|
|
182
|
+
})();
|
|
183
|
+
return delivery;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
describe('RFC 0214 — v2-a2a-push-delivery (host POSTs A2A push to a suite-owned receiver)', () => {
|
|
187
|
+
afterAll(async () => { await deliveryRx?.close(); });
|
|
188
|
+
|
|
189
|
+
it('§A/§C — a transition after registration is POSTed to the registered destination with the registered Authorization', async () => {
|
|
190
|
+
const id = R('a2a-push-delivery-authenticated');
|
|
191
|
+
const d = await deliverOnce(id);
|
|
192
|
+
if (d.kind === 'skip') return softSkip(d.skip, d.reason);
|
|
193
|
+
const { hits, taskId, cred, rx } = d;
|
|
194
|
+
expect(hits.length, req(id, `${DOC} §C ("at least one attempt")`, `a transition after registration MUST be POSTed at least once to the registered destination — ${noDeliveryCause(rx, 'A2A push')}`)).toBeGreaterThan(0);
|
|
195
|
+
const h = hits[0]!;
|
|
196
|
+
expect(h.method, req(id, 'A2A v1.0.1 §4.3.3', 'a push is an HTTP POST')).toBe('POST');
|
|
197
|
+
expect(header(h, 'authorization'), req(id, `${DOC} §A (A2A v1.0.1 §4.3.3 "MUST include authentication credentials")`, 'the push MUST carry the registered credential as Authorization: {scheme} {credentials}')).toBe(`Bearer ${cred}`);
|
|
198
|
+
expect((header(h, 'content-type') ?? '').split(';')[0]!.trim(), req(id, 'A2A v1.0.1 §4.3.3', 'the push body is application/a2a+json')).toBe('application/a2a+json');
|
|
199
|
+
const body = JSON.parse(h.body || '{}') as { statusUpdate?: { taskId?: string; status?: { state?: string } } };
|
|
200
|
+
expect(body.statusUpdate?.taskId, req(id, 'A2A v1.0.1 §4.3.3 (StreamResponse)', `the push body MUST be a StreamResponse statusUpdate naming the task (got ${h.body.slice(0, 200)})`)).toBe(taskId);
|
|
201
|
+
}, LEG_MS);
|
|
202
|
+
|
|
203
|
+
it('§C — an A2A push carries no OpenWOP signature', async () => {
|
|
204
|
+
const id = R('a2a-push-no-openwop-signature');
|
|
205
|
+
let d: Delivery;
|
|
206
|
+
try { d = await deliverOnce(); }
|
|
207
|
+
catch (err) { return softSkip('blocked', `the delivery this leg reads was not made — the §A/§C leg failed first: ${err instanceof Error ? err.message.slice(0, 200) : String(err)}`); }
|
|
208
|
+
if (d.kind === 'skip') return softSkip(d.skip, d.reason);
|
|
209
|
+
expect(d.hits.length, req(id, DOC, 'positive control: at least one push arrived to read headers from')).toBeGreaterThan(0);
|
|
210
|
+
for (const x of d.hits) {
|
|
211
|
+
expect(header(x, 'openwop-signature'), req(id, `${DOC} §C ("no OpenWOP signature")`, 'an A2A push MUST NOT carry an OpenWOP-Signature — A2A defines no secret exchange to verify it')).toBeUndefined();
|
|
212
|
+
}
|
|
213
|
+
}, LEG_MS);
|
|
169
214
|
|
|
170
215
|
it('§B — a 3xx from the destination is a failed delivery: the redirect target receives nothing, credential or not', async () => {
|
|
171
216
|
const id = R('a2a-push-no-redirect');
|
|
@@ -181,13 +226,13 @@ describe('RFC 0214 — v2-a2a-push-delivery (host POSTs A2A push to a suite-owne
|
|
|
181
226
|
expect(created.error, req(id, DOC, `Create MUST succeed (got ${JSON.stringify(created.error)})`)).toBeUndefined();
|
|
182
227
|
await rpc(g.url, 'SendMessage', { message: acceptMessage(taskId) });
|
|
183
228
|
await settle(taskId, (s) => s !== undefined && TERMINAL.has(s));
|
|
184
|
-
const arrived = await until(() => origin.hits.length > 0,
|
|
229
|
+
const arrived = await until(() => origin.hits.length > 0, ARRIVAL_MS);
|
|
185
230
|
if (!arrived && absenceIsUnmeasured(origin.rx)) return softSkip('blocked', noDeliveryCause(origin.rx, 'A2A push'));
|
|
186
231
|
expect(origin.hits.length, req(id, DOC, `positive control: the registered destination MUST receive the push it then redirects — ${noDeliveryCause(origin.rx, 'A2A push')}`)).toBeGreaterThan(0);
|
|
187
232
|
await wait(4000); // any retry or followed redirect lands in this window
|
|
188
233
|
expect(target.hits.length, req(id, `${DOC} §B; webhooks.md §Egress ("refuse to follow redirects")`, 'the redirect target MUST receive nothing: a 3xx is a delivery failure and the credential MUST NOT be sent after a redirect')).toBe(0);
|
|
189
234
|
} finally { await origin.rx.close(); await target.rx.close(); }
|
|
190
|
-
},
|
|
235
|
+
}, LEG_MS);
|
|
191
236
|
|
|
192
237
|
it('§A — after Delete no further push reaches that destination, while a live config on the same task still receives', async () => {
|
|
193
238
|
const id = R('a2a-push-discard-on-delete');
|
|
@@ -207,13 +252,13 @@ describe('RFC 0214 — v2-a2a-push-delivery (host POSTs A2A push to a suite-owne
|
|
|
207
252
|
expect(del.error, req(id, DOC, `Delete of the caller's own config MUST succeed (got ${JSON.stringify(del.error)})`)).toBeUndefined();
|
|
208
253
|
await rpc(g.url, 'SendMessage', { message: acceptMessage(taskId) });
|
|
209
254
|
await settle(taskId, (s) => s !== undefined && TERMINAL.has(s));
|
|
210
|
-
const arrived = await until(() => kept.hits.length > 0,
|
|
255
|
+
const arrived = await until(() => kept.hits.length > 0, ARRIVAL_MS);
|
|
211
256
|
if (!arrived && absenceIsUnmeasured(kept.rx)) return softSkip('blocked', noDeliveryCause(kept.rx, 'A2A push'));
|
|
212
257
|
expect(kept.hits.length, req(id, DOC, `positive control: the config that was NOT deleted MUST still receive the push — ${noDeliveryCause(kept.rx, 'A2A push')}`)).toBeGreaterThan(0);
|
|
213
258
|
await wait(3000);
|
|
214
259
|
expect(dropped.hits.length, req(id, `${DOC} §A ("dropped when the config is deleted")`, 'a deleted config MUST receive nothing further — its destination credential is discarded with it')).toBe(0);
|
|
215
260
|
} finally { await kept.rx.close(); await dropped.rx.close(); }
|
|
216
|
-
},
|
|
261
|
+
}, LEG_MS);
|
|
217
262
|
|
|
218
263
|
it('§D — a replay fork of a pushed run delivers nothing to the source run\'s destination', async () => {
|
|
219
264
|
const id = R('a2a-push-fork-no-push');
|
|
@@ -230,7 +275,7 @@ describe('RFC 0214 — v2-a2a-push-delivery (host POSTs A2A push to a suite-owne
|
|
|
230
275
|
if (why !== null) { await rpc(g.url, 'CancelTask', { id: taskId }); return softSkip('blocked', why); }
|
|
231
276
|
await rpc(g.url, 'SendMessage', { message: acceptMessage(taskId) });
|
|
232
277
|
await settle(taskId, (s) => s !== undefined && TERMINAL.has(s));
|
|
233
|
-
const arrived = await until(() => hits.length > 0,
|
|
278
|
+
const arrived = await until(() => hits.length > 0, ARRIVAL_MS);
|
|
234
279
|
if (!arrived && absenceIsUnmeasured(rx)) return softSkip('blocked', noDeliveryCause(rx, 'A2A push'));
|
|
235
280
|
expect(hits.length, req(id, DOC, `positive control: the source run MUST have pushed before the fork — ${noDeliveryCause(rx, 'A2A push')}`)).toBeGreaterThan(0);
|
|
236
281
|
await wait(3000);
|
|
@@ -238,9 +283,9 @@ describe('RFC 0214 — v2-a2a-push-delivery (host POSTs A2A push to a suite-owne
|
|
|
238
283
|
const fork = await driver.post(`/runs/${encodeURIComponent(taskId)}:fork`, { mode: 'replay' });
|
|
239
284
|
if (fork.status < 200 || fork.status >= 300) return softSkip('blocked', `replay is advertised but forkRun answered ${fork.status} ${JSON.stringify(fork.json)} — the fork leg is unmeasured`);
|
|
240
285
|
const child = (fork.json as { runId?: string } | undefined)?.runId;
|
|
241
|
-
if (typeof child === 'string') await settle(child, (s) => s !== undefined && TERMINAL.has(s)
|
|
286
|
+
if (typeof child === 'string') await settle(child, (s) => s !== undefined && TERMINAL.has(s));
|
|
242
287
|
await wait(4000);
|
|
243
288
|
expect(hits.length, req(id, `${DOC} §D; replay.md §Suppression ("MUST NOT deliver events a replay re-emits")`, `a replay fork MUST NOT push to the source run's destination (deliveries ${before} before the fork, ${hits.length} after)`)).toBe(before);
|
|
244
289
|
} finally { await rx.close(); }
|
|
245
|
-
},
|
|
290
|
+
}, FORK_LEG_MS);
|
|
246
291
|
});
|
|
@@ -74,7 +74,15 @@ describe('v2 lane-issuer-advertised (RFC 0170 §B.2–§B.4)', () => {
|
|
|
74
74
|
expect(LANES.has(name), req('openwop.requirement.0170.lane-issuer-advertised.members', DOC, `auth.lanes[].lane MUST be one of the ten lanes (got ${name})`)).toBe(true);
|
|
75
75
|
const issuers = l['issuers'];
|
|
76
76
|
expect(Array.isArray(issuers) && issuers.length > 0 && issuers.every((i) => typeof i === 'string' && i.length > 0), req('openwop.requirement.0170.lane-issuer-advertised.members', 'spec/v2/core/identity.md §2.2', `lane ${name} MUST advertise its trust root in issuers[] (min 1, non-empty strings)`)).toBe(true);
|
|
77
|
-
|
|
77
|
+
// identity.md §2.2: a lane whose row reads "—" (`anonymous`) has no
|
|
78
|
+
// revocation rule, so it MAY omit `revocation` — the schema made it
|
|
79
|
+
// optional there in 2.38.0 (openwop#1540), and until 2.39.1 this leg
|
|
80
|
+
// still demanded it, failing a host that followed the schema and the
|
|
81
|
+
// prose (measured on openwop-app). If such a lane does advertise one,
|
|
82
|
+
// it must still be a known member. Every other lane MUST name its rule.
|
|
83
|
+
if (!(LANE_RULES[name] === null && l['revocation'] === undefined)) {
|
|
84
|
+
expect(REVOCATION.has(String(l['revocation'])), req('openwop.requirement.0170.lane-issuer-advertised.members', 'spec/v2/core/identity.md §2.2', `lane ${name} MUST name its revocation rule`)).toBe(true);
|
|
85
|
+
}
|
|
78
86
|
expect(ASSURANCE.has(String(l['minimumAssurance'])), req('openwop.requirement.0170.lane-issuer-advertised.members', 'spec/v2/core/identity.md §2.3', `lane ${name} MUST advertise minimumAssurance: bearer | sender-constrained | key-bound`)).toBe(true);
|
|
79
87
|
if (l['delegationProofs'] !== undefined) {
|
|
80
88
|
expect(Array.isArray(l['delegationProofs']) && (l['delegationProofs'] as unknown[]).every((p) => PROOFS.has(String(p))), req('openwop.requirement.0170.lane-issuer-advertised.members', 'spec/v2/core/identity.md §2.4', `lane ${name}: delegationProofs[] MUST be drawn from mtls-key-binding | dpop | svid-chain`)).toBe(true);
|