@openwop/openwop-conformance 2.0.0-rc.3 → 2.0.0-rc.31
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 +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +293 -12
- package/scenario-majors.json +18 -3
- package/schemas/CORPUS-STAMP.json +30 -28
- package/src/global-setup.ts +11 -0
- package/src/lib/corpus-stamp.ts +24 -2
- package/src/scenarios/era-key-stamped-v1.test.ts +156 -0
- package/src/scenarios/v2-advertised-fixtures-exist.test.ts +90 -0
- package/src/scenarios/v2-advertised-path-space-served.test.ts +140 -0
- package/src/scenarios/v2-dual-stack-negotiation.test.ts +29 -1
- package/src/scenarios/v2-era-2-append-vocabulary.test.ts +9 -0
- package/src/scenarios/v2-era-stamp-universal.test.ts +103 -0
- package/src/scenarios/v2-interrupt-token-scheme.test.ts +1 -1
- package/src/scenarios/v2-version-header-honored.test.ts +124 -0
- package/src/scenarios/version-negotiation.test.ts +25 -3
|
@@ -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.
|
|
4
|
+
"version": "2.0.0-rc.31",
|
|
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": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "d87b5cd261b3bafe3680aa413a0bc08a4d44efa7b3e5629dc90f6d31832eb5ed",
|
|
13
|
+
"api/v2/asyncapi.yaml": "5bc7ab5ebca4b4a7aaed74adb3731dccceb4a948c866ca0bc9d3ce9a3546c0bd",
|
|
14
|
+
"api/v2/openapi.yaml": "3ab9232b7146f4ca1a131c0ccc40b1b1614a1983f1785221a7a5f7fe89a3c8b7",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"schemas/artifact-type-pack-manifest.schema.json": "13932ea58e819e01b3a0cc7a65590b70683dd5fe9208fc06c64c339943d3c4ce",
|
|
32
32
|
"schemas/audit-verify-result.schema.json": "e8fd013fb610ba5db27ab0113ec2eb18e7da5f9310dd3de2dd3dc87a259c21fe",
|
|
33
33
|
"schemas/budget-policy.schema.json": "3d4fe30ae129fe136ab9ece395f2a2a42a76360786b1b9a1d5f1cbc753eb5b39",
|
|
34
|
-
"schemas/capabilities.schema.json": "
|
|
35
|
-
"schemas/certification-bundle-v2.schema.json": "
|
|
34
|
+
"schemas/capabilities.schema.json": "b413e0235cad412081df09234237e0f322a85903b7411b91aaff54032e946671",
|
|
35
|
+
"schemas/certification-bundle-v2.schema.json": "2f85530dd04824a15810c94e96869362dee1e78396eca21fdd18e5501416bb7d",
|
|
36
36
|
"schemas/channel-presence-payload.schema.json": "f9d0ef4219814c2b765baf3e68cbe5eea2cb42379f516a93a0459dcd5f7db47e",
|
|
37
37
|
"schemas/channel-written-payload.schema.json": "54723a56bc5a35232f64544ca686ac1ac959a5fb05b32b2401facca6b3ef51a3",
|
|
38
38
|
"schemas/chat-card-pack-manifest.schema.json": "402f7e9081cc39857a52e772061f4e034a957dcbc17501fe6e8e0bcb3d337e42",
|
|
@@ -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": "
|
|
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",
|
|
@@ -100,9 +100,9 @@
|
|
|
100
100
|
"schemas/ui-plugin-message.schema.json": "346638babf1f8ea7b161cde75c0038b518cd2cc5bf0631772e20a9b74c96b5dd",
|
|
101
101
|
"schemas/v2/a2a-task-state.schema.json": "aa9bd0c15f653b7ecdb17427d61b1bccaf8156972c4b1a437dd3a8afb717b318",
|
|
102
102
|
"schemas/v2/a2ui-surface-delta-frame.schema.json": "43c10ebfd13c987520f0448df92fe74b52b846833763dfbbd5d35636bf75e8d9",
|
|
103
|
-
"schemas/v2/agent-deployment-transition.schema.json": "
|
|
104
|
-
"schemas/v2/agent-deployment.schema.json": "
|
|
105
|
-
"schemas/v2/agent-eval-suite.schema.json": "
|
|
103
|
+
"schemas/v2/agent-deployment-transition.schema.json": "d9fe6f2fa4dda8598bab4e5c051f68473ac0729dc0ebf31605954dda7abf5a80",
|
|
104
|
+
"schemas/v2/agent-deployment.schema.json": "dd1293ef8f20678bc36fc95fc4e5fcd135acbb10c61c1ddec8bc023e3f22622c",
|
|
105
|
+
"schemas/v2/agent-eval-suite.schema.json": "b2875edfc28b39c104fbf6f854ecfb81fe2d90b18f828ffd8d3232ef883cd208",
|
|
106
106
|
"schemas/v2/agent-inventory-response.schema.json": "bbf19724986a867a59ef51c115a43ece1cc4a003836b2f7f45dfefb3078db8f9",
|
|
107
107
|
"schemas/v2/agent-manifest.schema.json": "006ca6163ca8f9dc9fb91ca1ce01bd4c523f118680f6cbad971a4ac09e975381",
|
|
108
108
|
"schemas/v2/agent-org-chart.schema.json": "e64f1bcc6c0e9d3fc39c363e8e5e73b2b1fc49275b92f89b0cb941a7a80d67f2",
|
|
@@ -112,14 +112,14 @@
|
|
|
112
112
|
"schemas/v2/ai-envelope.schema.json": "0dc7dfbf032a37239ffd81fc2d67f999a40782ff5074866a56e414164cdee809",
|
|
113
113
|
"schemas/v2/annotation-create.schema.json": "c2fc19d055be29bc499e53c4975a562eb76981e12c521a82f63f5827ac6ff6c7",
|
|
114
114
|
"schemas/v2/annotation.schema.json": "75c6785775d00e62bc1282626c2c27eafde0f93a01c7e80e4e5e985055907475",
|
|
115
|
-
"schemas/v2/artifact-type-pack-manifest.schema.json": "
|
|
115
|
+
"schemas/v2/artifact-type-pack-manifest.schema.json": "85630d1b760d0b41cf5a6fad2acfada39abaf7f0befd95cc4334e1e883e9615d",
|
|
116
116
|
"schemas/v2/audit-verify-result.schema.json": "cb54c4d89ba5dfb73f49aa8959a980be65c2dc2a87a12f71ba36d3e836fe85da",
|
|
117
117
|
"schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
|
|
118
|
-
"schemas/v2/capabilities.schema.json": "
|
|
119
|
-
"schemas/v2/certification-bundle.schema.json": "
|
|
118
|
+
"schemas/v2/capabilities.schema.json": "792669b2cc7b1fd44ce9a4fd3e79696ad1277734e29299a4504b39eda15913ed",
|
|
119
|
+
"schemas/v2/certification-bundle.schema.json": "b278aed852c567fc4689adf463929e6c4bafd3c00d29c8f850a7eb179e20ca70",
|
|
120
120
|
"schemas/v2/channel-presence-payload.schema.json": "9475d46f358d21c19126e78204987a5633979917a2293653354ef5c5e044d54e",
|
|
121
121
|
"schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
|
|
122
|
-
"schemas/v2/chat-card-pack-manifest.schema.json": "
|
|
122
|
+
"schemas/v2/chat-card-pack-manifest.schema.json": "8df0d9d273ae22efc6876d5586833f1d4c2e551edc3ac6cf6e8f0719329f03dd",
|
|
123
123
|
"schemas/v2/compact-tool-descriptor.schema.json": "fc03f20c254c0ddc2cba7e7d5a6c46080ebd2cf9fae36f3c2510cbf88aadebdc",
|
|
124
124
|
"schemas/v2/compensation-policy.schema.json": "b3c1142637f5bf980f2b0bb67adb47e8cb7abb99a0405a4b6816ccf5d9c089d6",
|
|
125
125
|
"schemas/v2/compensation-projection.schema.json": "f46977c7076347b5473faa8c4bbe73d8321c33b33c98fff2f26ee7cbada4ed98",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"schemas/v2/conversation-turn.schema.json": "4c24aa85dd83f6ad00945b4a13ee8ff0a071893e884011e9289a07e97d22989d",
|
|
130
130
|
"schemas/v2/credential-provenance.schema.json": "f86b09e94b1c524dec7d1c6353965665ae8891e9517faef6e742d639c5578c99",
|
|
131
131
|
"schemas/v2/credential-reference.schema.json": "ea699ce30f58fb2c32d4279158b3f730f70be7b2dc8c3037693a76e66eb3e001",
|
|
132
|
-
"schemas/v2/debug-bundle.schema.json": "
|
|
132
|
+
"schemas/v2/debug-bundle.schema.json": "0bfa4b1d91fefbc12c95670dba9d5eacb72b879bb9a5c074e821ca9ed0fee6fd",
|
|
133
133
|
"schemas/v2/dispatch-config.schema.json": "4a7db312e6b4a14cf5b1ad2b71367878c77b6321cc456f5b3644dd6434e94cbd",
|
|
134
134
|
"schemas/v2/effect-ledger-projection.schema.json": "9e690f8e5dbf9aa7c82eef4bf7cf96c024f0f8626ad5bbaca3ff25ac63c7cab2",
|
|
135
135
|
"schemas/v2/effect-seam-manifest.schema.json": "3402a2956c810b2bfa9ec1ad0cb79d607ecea122cbe7d1e05184af006b06ef15",
|
|
@@ -142,21 +142,21 @@
|
|
|
142
142
|
"schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
|
|
143
143
|
"schemas/v2/envelopes/ui.a2ui-surface.schema.json": "0a06fbbdc8ba797a6818c79336a98e211ac5ba5747945b4e4098072cd3974d79",
|
|
144
144
|
"schemas/v2/error-envelope.schema.json": "d550beeab958b528c8734fd32e74c438006b99172c5c586f9e0af20ef4b2bee7",
|
|
145
|
-
"schemas/v2/eval-summary.schema.json": "
|
|
145
|
+
"schemas/v2/eval-summary.schema.json": "7f32c6a32ff9302910b4387d898273ef05200b8ab926b7a3acb23b03b059d794",
|
|
146
146
|
"schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
|
|
147
147
|
"schemas/v2/form-content-pack-manifest.schema.json": "307884e57946703b6f86ec283034bf56beb68bea4a000743ebbc9eebeed31b07",
|
|
148
148
|
"schemas/v2/frontend-plugin-manifest.schema.json": "fed6b8ddb3056c53b1bfc91525410a4445a3c37a8bf9eb9f6f9964ffea26d993",
|
|
149
149
|
"schemas/v2/goal.schema.json": "d202ef21ed3dbff87c25fd4b553fa978862a3200999878d25c5918f6794b3060",
|
|
150
150
|
"schemas/v2/heartbeat-evaluated.schema.json": "4086b1ba1d0547c08b038dfb263ac89416a3e406430db1250156fb2f9b74f8d6",
|
|
151
151
|
"schemas/v2/heartbeat-state-changed.schema.json": "eb68d0082aa996650e3a6b8ccf32457f959cebc7e6728cc7c331b9d171a4bef6",
|
|
152
|
-
"schemas/v2/ids.schema.json": "
|
|
152
|
+
"schemas/v2/ids.schema.json": "05c98e3f8a72fa79be123765bd9c2ea7c6620188d69df8524160399225316cd6",
|
|
153
153
|
"schemas/v2/localized-content-language-settings.schema.json": "92240f19a2b4b2f05ab41a797f703485a28813848035478e72f395edd04c1d35",
|
|
154
154
|
"schemas/v2/localized-content-page-response.schema.json": "476b5f2775b42e35e679814a4ce846a7be1d5311d3a748c10df4275a78b0fb27",
|
|
155
155
|
"schemas/v2/localized-content-page.schema.json": "717bb51fbc64d0b34eb0a4b35c8b463d08d505267bf251b4cbc67e0a016a567a",
|
|
156
156
|
"schemas/v2/localized-content-section.schema.json": "29c7bad0ad9ebc6c45211282e8afb3fdb35a4ca075ce0404b6e2c29f35fa5313",
|
|
157
157
|
"schemas/v2/memory-entry.schema.json": "290576ae84f4fef4cdf975932b87963283b11c50ea3d2654b32cd96aa6537c3d",
|
|
158
158
|
"schemas/v2/memory-list-options.schema.json": "f66703bccb68efb28391c273abb66aa117ee5b5ce475c477846ed700c336afcd",
|
|
159
|
-
"schemas/v2/node-pack-manifest.schema.json": "
|
|
159
|
+
"schemas/v2/node-pack-manifest.schema.json": "0da826ca63a29026f7a9551748048104910e0d9962db6bad2b50b97a24a9ea30",
|
|
160
160
|
"schemas/v2/orchestrator-decision.schema.json": "2da17538e84cfbe70151eb118f1ccf670cff58b8697148e394074d32ac4af6b4",
|
|
161
161
|
"schemas/v2/org-chart-responsibility-view.schema.json": "ebac7d2889bfd659bcf30311efb0d6eb0c7e940b972b6b62da69c7351b5bb586",
|
|
162
162
|
"schemas/v2/pack-lockfile.schema.json": "2685417702c105146b780cd99154296a46578fbb10d81c8c713348ee26fea898",
|
|
@@ -169,11 +169,11 @@
|
|
|
169
169
|
"schemas/v2/residency.schema.json": "af2658a7104cbcacd0e20a91226271c4a77f91ef46ae97612768fe743e6091d8",
|
|
170
170
|
"schemas/v2/run-ancestry-response.schema.json": "907e36341ba83a2147a5d76963950b6dcd47506b663ecbccae9dfa553f3724d3",
|
|
171
171
|
"schemas/v2/run-diff-response.schema.json": "6ab6cd265dc7e0a2086c6659c46aa0e7b43e6302949f0adac5d7deb1c7c0ecd3",
|
|
172
|
-
"schemas/v2/run-event-payloads.schema.json": "
|
|
173
|
-
"schemas/v2/run-event.schema.json": "
|
|
172
|
+
"schemas/v2/run-event-payloads.schema.json": "e8df13b81202afa507b902ab557504cbc579efb0309a05a01069609d7e7ebea7",
|
|
173
|
+
"schemas/v2/run-event.schema.json": "1a9946374af9323b71635d00fed22abe02c300fa2cf3ae3e9c30f3e553e408ec",
|
|
174
174
|
"schemas/v2/run-options.schema.json": "6d6052596ab0f17acfb00fdfc89907da7724f952e5ffbe1f81d94ba23d4c77cd",
|
|
175
175
|
"schemas/v2/run-orchestrator-decided-event.schema.json": "bb76e15ff72e669f12938684033b55dc567470e1a378e19ffc3fa0e4034ab5a7",
|
|
176
|
-
"schemas/v2/run-snapshot.schema.json": "
|
|
176
|
+
"schemas/v2/run-snapshot.schema.json": "0ee8a50c6be210db415a7c4345298e70a6ae3dd0dd764ccc514bc1c501437586",
|
|
177
177
|
"schemas/v2/security-advisory.schema.json": "6a382f737730a738d634266315e9d335e5760020a0cdd9f30727c2f987a01770",
|
|
178
178
|
"schemas/v2/self-hosted-runner-dispatch-frame.schema.json": "d8129f11d4e731d17ea951e66faaea2086081d13958b62ceaaa359fd6637dd7d",
|
|
179
179
|
"schemas/v2/self-hosted-runner-registration.schema.json": "93d60003a668cacb0eeac37699ca2884c6b79332d9494cb9e0a1752ac8f8a16c",
|
|
@@ -186,8 +186,9 @@
|
|
|
186
186
|
"schemas/v2/trigger-subscription-registration.schema.json": "202f7ab9c29e78249e0ad04341ac53377b506d24a0cbfbb7bb54494e80165832",
|
|
187
187
|
"schemas/v2/trigger-subscription.schema.json": "074bec7f016aa9f4d60359c3a093296cc847045510f01e0a9dceaf4fa5084d12",
|
|
188
188
|
"schemas/v2/ui-plugin-message.schema.json": "879d65867c01656b3962d64283e66364dcb8f626ad26c5fcf2d2d55a79ae0ce9",
|
|
189
|
-
"schemas/v2/
|
|
190
|
-
"schemas/v2/workflow-
|
|
189
|
+
"schemas/v2/webhook-delivery.schema.json": "1ac157d3baf61bd24074e2a253556d0cbad9cb5b2085d7d5056a35a6c863b4d5",
|
|
190
|
+
"schemas/v2/workflow-chain-pack-manifest.schema.json": "22801f14845a3df30660e6f31a41e1a3fbff2425bf56c8a475eab96f1c95882a",
|
|
191
|
+
"schemas/v2/workflow-definition.schema.json": "97c2afb4f22149f950f413663b357396c57d494666019ff2583e222ed1a9a65e",
|
|
191
192
|
"schemas/v2/workload-identity.schema.json": "1f589debfac6d70e0908da511550ec69a27b4bcc8314551cd843b6d923605efc",
|
|
192
193
|
"schemas/v2/workspace-file-create.schema.json": "b6b5260857318c3b67de0231ed15e3a746f525880a763e7b36a7854178dd10d0",
|
|
193
194
|
"schemas/v2/workspace-file.schema.json": "d2ed39e8c229dde33b56905c8df6f8b2ff056c97e1c5f615e3124baa54a6254f",
|
|
@@ -198,19 +199,19 @@
|
|
|
198
199
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
199
200
|
"spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
|
|
200
201
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
201
|
-
"spec/v1/core-standard-manifest.json": "
|
|
202
|
+
"spec/v1/core-standard-manifest.json": "de6a9b858dbc221a56b33be1159971081621588f4f5a2e2f11362f672c98e919",
|
|
202
203
|
"spec/v1/deprecations.json": "520d79927a8ed43ccf62c207a7cf6cf7b19b16e00bbbbdf0410667e939c19d85",
|
|
203
204
|
"spec/v1/deprecations.schema.json": "4cdbbb8eb9c0092913a5704c943880f0c8581bcf0f640a7df5a84105d53c8aee",
|
|
204
205
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
205
206
|
"spec/v1/event-codemap.schema.json": "d52caf29071e3784d4c20259cdbd5a7b533986548a440bb632a80b82cadd7ea5",
|
|
206
|
-
"spec/v1/extensions.json": "
|
|
207
|
+
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
207
208
|
"spec/v1/gaps.json": "96ca051cb20f1a669b355d67a89421c56fef28aa5e6710c9519f592f26af33a3",
|
|
208
209
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
209
210
|
"spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
|
|
210
211
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
211
212
|
"spec/v1/operation-path-manifest.json": "5f5f4e3842669371730ebbd1aace3fb794192615f0018dc144f484ba5db82ac3",
|
|
212
213
|
"spec/v1/spec-gaps.json": "6cc9962c6b969f632e07a78b52a4f61447ff579e2990cbae989866f584a86042",
|
|
213
|
-
"spec/v2/declaration.json": "
|
|
214
|
+
"spec/v2/declaration.json": "52b8c24a2dd463d44086129bf792ad108746f5102778cbbf0ff469fa534eca48",
|
|
214
215
|
"spec/v2/declaration.schema.json": "b0881a76228dfbd1b85a2ec7b7259a60197f768344535cdb475db6ff719b0e54",
|
|
215
216
|
"spec/v2/errors.json": "ad1725dc7c11002450e8abc682858eb5c11952a0718ea8ef6bdb2fb5ae6e7e21",
|
|
216
217
|
"spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
|
|
@@ -225,10 +226,11 @@
|
|
|
225
226
|
"spec/v2/facets/replay.schema.json": "0a5aefb0108c708c6794fadd533cf256709704c4f16dbf44cd63fa8980c78018",
|
|
226
227
|
"spec/v2/facets/sandbox.schema.json": "e75f54486d387b2d089791aa93b5093cd1450f1920753a0f97fc23acdfe978ed",
|
|
227
228
|
"spec/v2/facets/webhooks.schema.json": "0b766033ef4b4c6c2ac6f9979a19843ba024d3a1d1576972dcb55aaba52f1b9a",
|
|
229
|
+
"spec/v2/id-field-bindings.json": "5a053a9eb73a2a474c753807ffc1e6ba66fb4f686c340a239f7a4cebdd1142a7",
|
|
228
230
|
"spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
|
|
229
231
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
230
232
|
"spec/v2/profiles.json": "1f0ee40491131cf561ef19a0394c47ba476b4567514fe16befb6cada4a10a5a4",
|
|
231
|
-
"spec/v2/release.json": "
|
|
233
|
+
"spec/v2/release.json": "1fa053e50e30fddcbde302ecdc183168940c4c02c63c312a83af002bf7d9367f"
|
|
232
234
|
},
|
|
233
|
-
"corpusCommit": "
|
|
235
|
+
"corpusCommit": "1b1cf9dfcb2ebe9b254cfabf0ca328475914e028"
|
|
234
236
|
}
|
package/src/global-setup.ts
CHANGED
|
@@ -75,6 +75,17 @@ export function setup(): void {
|
|
|
75
75
|
// Suite 2.0.0: in the published layout the contract is the spec-artifacts peer (RFC 0168 §D.2).
|
|
76
76
|
const stamp = LAYOUT === 'published' ? verifyPeerContract(PKG_ROOT_PATH) : verifyCorpusStamp(PKG_ROOT_PATH, LAYOUT);
|
|
77
77
|
process.stderr.write(`${describeVerdict(stamp)}\n`);
|
|
78
|
+
// A version skew and a digest mismatch both have to refuse, but they are
|
|
79
|
+
// different faults with different fixes, and reporting them in the same words
|
|
80
|
+
// sends the reader to debug a corrupt install when nothing is corrupt.
|
|
81
|
+
if (stamp.kind === 'peer-version') {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`openwop-conformance: refusing to run — this suite was packed against @openwop/spec-artifacts@${stamp.lockVersion}, ` +
|
|
84
|
+
`but @openwop/spec-artifacts@${stamp.peerVersion} is installed. They are declared EXACT peers. ` +
|
|
85
|
+
`Install both at the same explicit version; do NOT install at a dist-tag such as \`next\`, which moves per package ` +
|
|
86
|
+
`and can name a pair that was never published together.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
78
89
|
if (stamp.kind === 'mismatch') {
|
|
79
90
|
throw new Error('openwop-conformance: refusing to run — schemas/CORPUS-STAMP.json digests do not match the vendored api/ + schemas/ files. Reinstall the package; do not hand-patch vendored contract files.');
|
|
80
91
|
}
|
package/src/lib/corpus-stamp.ts
CHANGED
|
@@ -39,7 +39,17 @@ export interface CorpusStamp {
|
|
|
39
39
|
export type StampVerdict =
|
|
40
40
|
| { readonly kind: 'verified'; readonly files: number }
|
|
41
41
|
| { readonly kind: 'not-applicable'; readonly reason: string }
|
|
42
|
-
| { readonly kind: 'mismatch'; readonly missing: readonly string[]; readonly altered: readonly string[]; readonly extra: readonly string[] }
|
|
42
|
+
| { readonly kind: 'mismatch'; readonly missing: readonly string[]; readonly altered: readonly string[]; readonly extra: readonly string[] }
|
|
43
|
+
/**
|
|
44
|
+
* The peer is INSTALLED and INTACT but is a different version than the suite
|
|
45
|
+
* was packed against. Its own kind because the remedy is completely different
|
|
46
|
+
* from a digest mismatch — nothing is corrupt, two versions are simply out of
|
|
47
|
+
* step — and because the generic message sends readers to debug a broken
|
|
48
|
+
* install. Reported by a tier-2 host that hit it through the `next` dist-tag:
|
|
49
|
+
* the tag moves per package, so `@next` can name an exact-peer PAIR that was
|
|
50
|
+
* never published together.
|
|
51
|
+
*/
|
|
52
|
+
| { readonly kind: 'peer-version'; readonly peerVersion: string; readonly lockVersion: string };
|
|
43
53
|
|
|
44
54
|
export const STAMP_RELATIVE_PATH = join('schemas', 'CORPUS-STAMP.json');
|
|
45
55
|
|
|
@@ -94,7 +104,11 @@ export function verifyPeerContract(pkgRoot: string): StampVerdict {
|
|
|
94
104
|
if (!existsSync(stampPath)) return { kind: 'mismatch', missing: ['@openwop/spec-artifacts/CORPUS-STAMP.json'], altered: [], extra: [] };
|
|
95
105
|
const stamp = JSON.parse(readFileSync(stampPath, 'utf8')) as { package: string; version: string; files: Record<string, string> };
|
|
96
106
|
const digest = createHash('sha256').update(JSON.stringify({ package: stamp.package, version: stamp.version, files: stamp.files })).digest('hex');
|
|
97
|
-
|
|
107
|
+
// A plain version difference is NOT corruption; report it as itself so the
|
|
108
|
+
// message names the two versions and the fix, instead of sending the reader
|
|
109
|
+
// to hunt a damaged install.
|
|
110
|
+
if (stamp.version !== lock.version) return { kind: 'peer-version', peerVersion: stamp.version, lockVersion: lock.version };
|
|
111
|
+
if (digest !== lock.stampSha256) return { kind: 'mismatch', missing: [], altered: [`@openwop/spec-artifacts ${stamp.version} stamp digest ${digest.slice(0, 12)} ≠ the suite's lock ${lock.stampSha256.slice(0, 12)} — same version, different contents`], extra: [] };
|
|
98
112
|
const missing: string[] = []; const altered: string[] = [];
|
|
99
113
|
for (const [rel, d] of Object.entries(stamp.files)) { const p = join(peerRoot, ...rel.split('/')); if (!existsSync(p)) missing.push(rel); else if (sha256File(p) !== d) altered.push(rel); }
|
|
100
114
|
if (missing.length || altered.length) return { kind: 'mismatch', missing, altered, extra: [] };
|
|
@@ -143,6 +157,14 @@ export function describeVerdict(v: StampVerdict): string {
|
|
|
143
157
|
return `[openwop-conformance] corpus stamp VERIFIED — ${v.files} vendored api/ + schemas/ files match their SHA-256 digests`;
|
|
144
158
|
case 'not-applicable':
|
|
145
159
|
return `[openwop-conformance] corpus stamp not checked — ${v.reason}`;
|
|
160
|
+
case 'peer-version':
|
|
161
|
+
return (
|
|
162
|
+
`[openwop-conformance] peer version MISMATCH — this suite was packed against ` +
|
|
163
|
+
`@openwop/spec-artifacts@${v.lockVersion} but @openwop/spec-artifacts@${v.peerVersion} is installed. ` +
|
|
164
|
+
`Nothing is corrupt: the two are declared EXACT peers and are simply out of step. ` +
|
|
165
|
+
`Install both at the same explicit version — never at a dist-tag such as \`next\`, which moves per package ` +
|
|
166
|
+
`and can therefore name a pair that was never published together.`
|
|
167
|
+
);
|
|
146
168
|
case 'mismatch':
|
|
147
169
|
return (
|
|
148
170
|
`[openwop-conformance] corpus stamp MISMATCH — the vendored contract is not the one this suite shipped ` +
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec/v2/core/conformance.md` — an advertised conformance fixture is a claim
|
|
3
|
+
* the host must be able to honour (suite 2.0.0, target major 2; unaided).
|
|
4
|
+
*
|
|
5
|
+
* `fixtures[]` in discovery gates scenarios: `isFixtureAdvertised(id)` decides
|
|
6
|
+
* whether a scenario runs at all. So a host whose advertised list and seeded set
|
|
7
|
+
* drift apart fails somewhere else entirely — the scenario gated on the missing
|
|
8
|
+
* fixture attempts, fails on a run that cannot be created, and the failure is
|
|
9
|
+
* attributed to that scenario's requirement rather than to the advertisement
|
|
10
|
+
* that was wrong. That misattribution is what this scenario exists to catch.
|
|
11
|
+
*
|
|
12
|
+
* **This scenario shipped with a second leg that was wrong, and the correction
|
|
13
|
+
* matters more than the check.** That leg asserted the advertised ids are a
|
|
14
|
+
* SUBSET of `conformance/fixtures/` — "the vocabulary is closed, so an id the
|
|
15
|
+
* corpus does not define is a typo or an invention". The vocabulary is not
|
|
16
|
+
* closed. Host-supplied fixtures are the normal case: dozens of ids the
|
|
17
|
+
* scenarios gate on are deliberately not shipped, and `v2-approver-enforced`
|
|
18
|
+
* says so in its own docstring — it needs an approval fixture whose
|
|
19
|
+
* `approversList` names a principal the suite is not, and records `blocked`
|
|
20
|
+
* naming it precisely because "no such fixture ships in `conformance/fixtures/`".
|
|
21
|
+
*
|
|
22
|
+
* So the leg failed a host for doing exactly what the corpus asks. It was found
|
|
23
|
+
* by running the suite against the reference host, which advertised two
|
|
24
|
+
* host-supplied fixtures and was marked non-conformant for it. Set membership
|
|
25
|
+
* cannot distinguish a typo from a legitimate host fixture, and a check that
|
|
26
|
+
* cannot tell those apart is not a check — it is a coin flip that happens to
|
|
27
|
+
* land on "fail" for correct hosts.
|
|
28
|
+
*
|
|
29
|
+
* What survives is the leg that was always sound: an advertised fixture MUST be
|
|
30
|
+
* creatable. That holds whoever defines it, and it is the one that catches the
|
|
31
|
+
* drift the misattribution comes from.
|
|
32
|
+
*
|
|
33
|
+
* @see spec/v2/core/conformance.md
|
|
34
|
+
* @see conformance/src/scenarios/v2-approver-enforced.test.ts (a host-supplied fixture, by design)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { describe, it, expect } from 'vitest';
|
|
38
|
+
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
39
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
40
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
41
|
+
import { req } from '../lib/requirement-ids.js';
|
|
42
|
+
|
|
43
|
+
const ID = 'openwop.requirement.0168.advertised-fixtures-exist';
|
|
44
|
+
const DOC = 'spec/v2/core/conformance.md §Fixtures';
|
|
45
|
+
/** Creating one run per advertised fixture would be a load test, not a check. */
|
|
46
|
+
const SAMPLE = 5;
|
|
47
|
+
|
|
48
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
|
|
49
|
+
try {
|
|
50
|
+
return await fn();
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function advertisedIds(doc: Record<string, unknown>): string[] {
|
|
57
|
+
const raw = doc['fixtures'];
|
|
58
|
+
return Array.isArray(raw) ? raw.filter((x): x is string => typeof x === 'string') : [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('v2-advertised-fixtures-exist (conformance.md §Fixtures)', () => {
|
|
62
|
+
it('a sampled advertised fixture is actually creatable, not just listed', async () => {
|
|
63
|
+
const doc = await v2Discovery().catch(() => null);
|
|
64
|
+
if (!doc) return softSkip('blocked', 'v2 discovery unreachable');
|
|
65
|
+
const ids = advertisedIds(doc);
|
|
66
|
+
if (ids.length === 0) return softSkip('inapplicable', 'the host advertises no fixtures[]');
|
|
67
|
+
|
|
68
|
+
// Deterministic sample: the first N by sort order, so a failure is
|
|
69
|
+
// reproducible and a host cannot pass by luck of ordering.
|
|
70
|
+
const sample = [...ids].sort().slice(0, SAMPLE);
|
|
71
|
+
const unreachable: string[] = [];
|
|
72
|
+
let attempted = 0;
|
|
73
|
+
for (const id of sample) {
|
|
74
|
+
const res = await http(() => driver.post('/runs', { workflowId: id, inputs: {} }));
|
|
75
|
+
if (res === null) {
|
|
76
|
+
return softSkip('blocked', `POST /runs was unreachable while sampling advertised fixtures (stopped at ${id})`);
|
|
77
|
+
}
|
|
78
|
+
attempted += 1;
|
|
79
|
+
// 201 is the claim honoured. A 4xx that names the workflow as unknown is
|
|
80
|
+
// the drift this leg exists to catch; any other status is a different
|
|
81
|
+
// problem and is not judged here.
|
|
82
|
+
if (res.status !== 201) unreachable.push(`${id} → ${res.status}`);
|
|
83
|
+
}
|
|
84
|
+
if (attempted === 0) return softSkip('blocked', 'no advertised fixture could be attempted');
|
|
85
|
+
expect(
|
|
86
|
+
unreachable,
|
|
87
|
+
req(ID, DOC, `an advertised fixture MUST be creatable: fixtures[] gates whether a scenario runs at all, so a listed-but-unseeded fixture makes some OTHER scenario fail on a run that cannot exist, and the failure is attributed to the wrong requirement (${unreachable.length} of ${attempted} sampled: ${unreachable.join(', ')})`),
|
|
88
|
+
).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0172 §A.1 / `spec/v2/core/versioning.md` §1.2 — a host that advertises
|
|
3
|
+
* major 2 serves the major-2 PATH SPACE, not just the major-2 discovery
|
|
4
|
+
* document (suite 2.0.0, target major 2; unaided).
|
|
5
|
+
*
|
|
6
|
+
* `v2-version-header-honored` checks that `OpenWOP-Version` is honored or
|
|
7
|
+
* refused rather than ignored. It probes `/.well-known/openwop`, because that
|
|
8
|
+
* is the one resource whose representation the header selects. **It therefore
|
|
9
|
+
* cannot see a host that negotiates correctly on the well-known resource and
|
|
10
|
+
* has mounted almost none of the rest of the v2 surface.**
|
|
11
|
+
*
|
|
12
|
+
* That is not hypothetical. A tier-1 host advertising
|
|
13
|
+
* `protocolVersions: ["1.1","2.0"]` was found serving **two of fifteen**
|
|
14
|
+
* top-level segments of the v2 path space: its unversioned mount was a
|
|
15
|
+
* deliberate allowlist (`['/runs','/interrupts']`, chosen over a blanket
|
|
16
|
+
* `/v1`-strip because the host serves a large non-`/v1` surface a blanket
|
|
17
|
+
* rewrite would shadow) and the list was simply incomplete. Every probe used to
|
|
18
|
+
* call the dual stack live — `protocolVersions`, `preferredVersion`, the
|
|
19
|
+
* response header, the two differing representations — hits `/.well-known`, so
|
|
20
|
+
* every one of them passed. `POST /webhooks` under major 2 returned `404` while
|
|
21
|
+
* `POST /v1/webhooks` returned `201`. The host found this itself by applying
|
|
22
|
+
* the artifact rule to a scenario it had first classified as a harness defect.
|
|
23
|
+
*
|
|
24
|
+
* The discriminator is a PAIR, not a single probe, because "this host does not
|
|
25
|
+
* implement webhooks at all" and "this host implements webhooks but did not
|
|
26
|
+
* mount them under major 2" are different facts that a lone `404` cannot
|
|
27
|
+
* separate:
|
|
28
|
+
*
|
|
29
|
+
* /v1<path> exists AND <path> is 404 under major 2 ⇒ the advertisement
|
|
30
|
+
* overstates: the surface exists and major 2 does not reach it.
|
|
31
|
+
*
|
|
32
|
+
* both 404 ⇒ the host does not serve that surface in either major. Not this
|
|
33
|
+
* scenario's business, and recorded as neither pass nor failure.
|
|
34
|
+
*
|
|
35
|
+
* Only parameterless GETs from `spec/v2/path-manifest.json` are probed: they
|
|
36
|
+
* need no fixture, mutate nothing, and a route that is not mounted answers 404
|
|
37
|
+
* regardless of auth, so the check is unaided and safe against a live host.
|
|
38
|
+
*
|
|
39
|
+
* @see spec/v2/core/versioning.md §1.2
|
|
40
|
+
* @see RFCS/0172-v2-versioning-and-release.md §A.1
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { describe, it, expect } from 'vitest';
|
|
44
|
+
import { loadEnv } from '../lib/env.js';
|
|
45
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
46
|
+
import { req } from '../lib/requirement-ids.js';
|
|
47
|
+
import { readFileSync } from 'node:fs';
|
|
48
|
+
import { join } from 'node:path';
|
|
49
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
50
|
+
|
|
51
|
+
const ID = 'openwop.requirement.0172.advertised-path-space-served';
|
|
52
|
+
const DOC = 'spec/v2/core/versioning.md §1.2';
|
|
53
|
+
|
|
54
|
+
/** The well-known resource is the one the HEADER selects; it has no /v1 twin to pair against. */
|
|
55
|
+
const NOT_PAIRABLE = new Set(['/.well-known/openwop']);
|
|
56
|
+
|
|
57
|
+
interface Probe { readonly status: number | null }
|
|
58
|
+
|
|
59
|
+
async function get(path: string, major2: boolean): Promise<Probe> {
|
|
60
|
+
const { baseUrl, apiKey } = loadEnv();
|
|
61
|
+
const headers: Record<string, string> = { Accept: 'application/json' };
|
|
62
|
+
if (major2) headers['OpenWOP-Version'] = '2.0';
|
|
63
|
+
if (apiKey) headers['authorization'] = `Bearer ${apiKey}`;
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, '')}${path}`, { headers });
|
|
66
|
+
return { status: res.status };
|
|
67
|
+
} catch {
|
|
68
|
+
return { status: null };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parameterlessGets(): string[] {
|
|
73
|
+
try {
|
|
74
|
+
const manifest = JSON.parse(
|
|
75
|
+
readFileSync(join(SCHEMAS_DIR, '..', 'spec', 'v2', 'path-manifest.json'), 'utf8'),
|
|
76
|
+
) as { operations?: ReadonlyArray<{ method: string; path: string }> };
|
|
77
|
+
return (manifest.operations ?? [])
|
|
78
|
+
.filter((o) => o.method === 'GET' && !o.path.includes('{') && !NOT_PAIRABLE.has(o.path))
|
|
79
|
+
.map((o) => o.path)
|
|
80
|
+
.sort();
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe('v2-advertised-path-space-served (RFC 0172 §A.1)', () => {
|
|
87
|
+
it('a host advertising major 2 reaches the surfaces it already serves under /v1', async () => {
|
|
88
|
+
const { baseUrl } = loadEnv();
|
|
89
|
+
const bare = await (async () => {
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/.well-known/openwop`, { headers: { Accept: 'application/json' } });
|
|
92
|
+
return res.status === 200 ? ((await res.json()) as Record<string, unknown>) : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
if (!bare) return softSkip('blocked', 'the discovery document is unreadable, so the advertised majors are unknown');
|
|
98
|
+
|
|
99
|
+
const versions = Array.isArray(bare['protocolVersions']) ? (bare['protocolVersions'] as unknown[]).map(String) : [];
|
|
100
|
+
if (!new Set(versions.map((v) => v.split('.')[0])).has('2')) {
|
|
101
|
+
return softSkip('inapplicable', `the host advertises [${versions.join(', ') || 'no protocolVersions'}] — it does not claim major 2, so there is no path space to hold it to`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const paths = parameterlessGets();
|
|
105
|
+
if (paths.length === 0) return softSkip('blocked', 'spec/v2/path-manifest.json is unreadable from this layout');
|
|
106
|
+
|
|
107
|
+
const overstated: string[] = [];
|
|
108
|
+
const served: string[] = [];
|
|
109
|
+
let pairable = 0;
|
|
110
|
+
for (const path of paths) {
|
|
111
|
+
const v1 = await get(`/v1${path}`, false);
|
|
112
|
+
if (v1.status === null) return softSkip('blocked', `the host became unreachable while probing /v1${path}`);
|
|
113
|
+
// The host does not serve this surface in EITHER major. Legitimate, and a
|
|
114
|
+
// different question from the one asked here.
|
|
115
|
+
if (v1.status === 404) continue;
|
|
116
|
+
pairable += 1;
|
|
117
|
+
|
|
118
|
+
const v2 = await get(path, true);
|
|
119
|
+
if (v2.status === null) return softSkip('blocked', `the host became unreachable while probing ${path}`);
|
|
120
|
+
// Any status but 404 means the route is MOUNTED — 401/403/422 all answer
|
|
121
|
+
// "this path exists". Only 404 says major 2 cannot reach it.
|
|
122
|
+
if (v2.status === 404) overstated.push(`${path} (/v1 → ${v1.status}, major 2 → 404)`);
|
|
123
|
+
else served.push(path);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (pairable === 0) {
|
|
127
|
+
return softSkip('inapplicable', 'no parameterless GET in the v2 manifest is served under /v1 either, so there is no pair to compare and the advertisement cannot be checked this way');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
expect(
|
|
131
|
+
overstated,
|
|
132
|
+
req(ID, DOC, `a host advertising major 2 MUST reach, under major 2, the surfaces it already serves under /v1 — advertising the major is a claim about the PATH SPACE and not only about the well-known resource, whose representation the header selects and which therefore passes even when almost nothing else is mounted (${overstated.length} of ${pairable} pairable surface(s) unreachable: ${overstated.slice(0, 6).join('; ')})`),
|
|
133
|
+
).toEqual([]);
|
|
134
|
+
|
|
135
|
+
expect(
|
|
136
|
+
served.length,
|
|
137
|
+
req(ID, DOC, 'at least one non-well-known surface MUST be reachable under major 2, or the advertisement rests entirely on the one resource the header selects'),
|
|
138
|
+
).toBeGreaterThan(0);
|
|
139
|
+
});
|
|
140
|
+
});
|