@openwop/openwop-conformance 2.46.22 → 2.46.24
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 +10 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +69 -4
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +18 -18
- package/src/lib/host-event-replay-witness.ts +86 -0
- package/src/scenarios/v2-host-event-strict-replay.test.ts +186 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## [2.46.24] — 2026-10-25 — RFC 0250 Accepted
|
|
2
|
+
|
|
3
|
+
- **No assertion changes.** RFC 0250's G6 closes on openwop-app's certified cut `f94471e98` (all six `0250.*` legs `executed-pass`), so the packed `spec/v1/gaps.json` changes.
|
|
4
|
+
|
|
5
|
+
## [2.46.23] — 2026-10-24 — RFC 0250 Active: opt-in host-event replay
|
|
6
|
+
|
|
7
|
+
- **New scenario `v2-host-event-strict-replay`.** The legs are `0250.shape` (server-free), `resume-backlog`, `ephemeral-not-replayed`, `cursor-refused`, `refusal-uniform` and `legacy-unchanged`. The suite emits durable D1, D2, ephemeral E and durable D3 through the RFC 0241 trigger, then reconnects with `replay=required` from D1 and emits D4 live. Judges are in `lib/host-event-replay-witness.ts`; their self-tests fail each wrong host answer, including a foreign cursor answered differently (an oracle).
|
|
8
|
+
- **Gating.** The legs need `hostEvents.replay` and both `host-test.*` types listed. The uniform leg also needs `OPENWOP_TEST_TENANT_B_API_KEY`; if it is unset, the leg records `blocked`.
|
|
9
|
+
- **Host impact:** none today. No host advertises `hostEvents.replay`.
|
|
10
|
+
|
|
1
11
|
## [2.46.22] — 2026-10-24 — RFCs 0249 and 0096 Accepted
|
|
2
12
|
|
|
3
13
|
- **No assertion changes.** RFC 0249's G1 closes on openwop-app's certified cut `34bfefddc` (all nine `0249.*` legs `executed-pass`), so the packed `spec/v1/gaps.json` changes.
|
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ Each scenario file records its disposition and assertion count in `<report-dir>/
|
|
|
137
137
|
|
|
138
138
|
## What's Covered
|
|
139
139
|
|
|
140
|
-
The current suite has
|
|
140
|
+
The current suite has 661 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 221 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
|
|
141
141
|
|
|
142
142
|
The v2 scenarios (`src/scenarios/v2-*.test.ts`) cover, by area:
|
|
143
143
|
|
|
@@ -166,7 +166,7 @@ Three kinds of scenario share the tree:
|
|
|
166
166
|
- **Host** checks drive a live host over HTTP.
|
|
167
167
|
- **Gated** checks run only when the host advertises the family, profile or fixture they need, and otherwise record `inapplicable` or `blocked` with a reason. `--require-behavior` turns an advertised-but-unwitnessable behaviour into a failure.
|
|
168
168
|
|
|
169
|
-
Current source tree:
|
|
169
|
+
Current source tree: 661 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
|
|
170
170
|
|
|
171
171
|
## How the 2.x suite is built
|
|
172
172
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.46.
|
|
3
|
+
"version": "2.46.24",
|
|
4
4
|
"description": "Black-box conformance suite for OpenWOP hosts (v2 current; v1 scenarios retained for the frozen v1 tree).",
|
|
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.46.
|
|
59
|
+
"@openwop/spec-artifacts": "2.46.24"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
|
-
"files":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 749,
|
|
6
|
+
"tests": 2840,
|
|
7
|
+
"withStableId": 2840,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2713
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -31928,6 +31928,71 @@
|
|
|
31928
31928
|
"explicitId": "openwop.requirement.0236.host-event.tenant-isolation",
|
|
31929
31929
|
"citations": []
|
|
31930
31930
|
},
|
|
31931
|
+
{
|
|
31932
|
+
"id": "openwop.it.v2-host-event-strict-replay.the-replay-facet-member-validates-and-its-negatives-are-refused",
|
|
31933
|
+
"file": "v2-host-event-strict-replay.test.ts",
|
|
31934
|
+
"line": 127,
|
|
31935
|
+
"title": "the replay facet member validates, and its negatives are refused",
|
|
31936
|
+
"explicitId": "openwop.requirement.0250.shape",
|
|
31937
|
+
"citations": [
|
|
31938
|
+
{
|
|
31939
|
+
"section": "hostEvents.schema.json §replay (RFC 0250)",
|
|
31940
|
+
"requirement": "a positive retention MUST validate"
|
|
31941
|
+
},
|
|
31942
|
+
{
|
|
31943
|
+
"section": "hostEvents.schema.json §replay (RFC 0250)",
|
|
31944
|
+
"requirement": "a zero retention MUST be refused"
|
|
31945
|
+
},
|
|
31946
|
+
{
|
|
31947
|
+
"section": "hostEvents.schema.json §replay (RFC 0250)",
|
|
31948
|
+
"requirement": "minRetentionSeconds is required"
|
|
31949
|
+
},
|
|
31950
|
+
{
|
|
31951
|
+
"section": "hostEvents.schema.json §replay (RFC 0250)",
|
|
31952
|
+
"requirement": "the member is closed (the Draft mode const was dropped)"
|
|
31953
|
+
}
|
|
31954
|
+
]
|
|
31955
|
+
},
|
|
31956
|
+
{
|
|
31957
|
+
"id": "openwop.it.v2-host-event-strict-replay.a-resumed-stream-delivers-the-backlog-after-the-cursor-then-live-in-order",
|
|
31958
|
+
"file": "v2-host-event-strict-replay.test.ts",
|
|
31959
|
+
"line": 138,
|
|
31960
|
+
"title": "a resumed stream delivers the backlog after the cursor, then live, in order",
|
|
31961
|
+
"explicitId": "openwop.requirement.0250.resume-backlog",
|
|
31962
|
+
"citations": []
|
|
31963
|
+
},
|
|
31964
|
+
{
|
|
31965
|
+
"id": "openwop.it.v2-host-event-strict-replay.an-ephemeral-event-is-not-replayed",
|
|
31966
|
+
"file": "v2-host-event-strict-replay.test.ts",
|
|
31967
|
+
"line": 146,
|
|
31968
|
+
"title": "an ephemeral event is not replayed",
|
|
31969
|
+
"explicitId": "openwop.requirement.0250.ephemeral-not-replayed",
|
|
31970
|
+
"citations": []
|
|
31971
|
+
},
|
|
31972
|
+
{
|
|
31973
|
+
"id": "openwop.it.v2-host-event-strict-replay.an-unknown-cursor-is-refused-409-host-event-resync-required-before-any-sse-byte",
|
|
31974
|
+
"file": "v2-host-event-strict-replay.test.ts",
|
|
31975
|
+
"line": 154,
|
|
31976
|
+
"title": "an unknown cursor is refused 409 host_event_resync_required before any SSE byte",
|
|
31977
|
+
"explicitId": "openwop.requirement.0250.cursor-refused",
|
|
31978
|
+
"citations": []
|
|
31979
|
+
},
|
|
31980
|
+
{
|
|
31981
|
+
"id": "openwop.it.v2-host-event-strict-replay.a-foreign-or-ephemeral-cursor-is-refused-byte-identically-to-an-unknown-one",
|
|
31982
|
+
"file": "v2-host-event-strict-replay.test.ts",
|
|
31983
|
+
"line": 160,
|
|
31984
|
+
"title": "a foreign or ephemeral cursor is refused byte-identically to an unknown one",
|
|
31985
|
+
"explicitId": "openwop.requirement.0250.refusal-uniform",
|
|
31986
|
+
"citations": []
|
|
31987
|
+
},
|
|
31988
|
+
{
|
|
31989
|
+
"id": "openwop.it.v2-host-event-strict-replay.without-replay-required-an-unknown-cursor-keeps-its-existing-answer",
|
|
31990
|
+
"file": "v2-host-event-strict-replay.test.ts",
|
|
31991
|
+
"line": 180,
|
|
31992
|
+
"title": "without replay=required an unknown cursor keeps its existing answer",
|
|
31993
|
+
"explicitId": "openwop.requirement.0250.legacy-unchanged",
|
|
31994
|
+
"citations": []
|
|
31995
|
+
},
|
|
31931
31996
|
{
|
|
31932
31997
|
"id": "openwop.it.v2-i18n-negotiation.the-advertised-i18n-record-is-well-formed-and-lists-its-default-locale",
|
|
31933
31998
|
"file": "v2-i18n-negotiation.test.ts",
|
package/scenario-majors.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
|
|
3
3
|
"counts": {
|
|
4
|
-
"files":
|
|
4
|
+
"files": 661,
|
|
5
5
|
"v1": 455,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 221
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1509,6 +1509,9 @@
|
|
|
1509
1509
|
"v2-host-event-delivery.test.ts": [
|
|
1510
1510
|
2
|
|
1511
1511
|
],
|
|
1512
|
+
"v2-host-event-strict-replay.test.ts": [
|
|
1513
|
+
2
|
|
1514
|
+
],
|
|
1512
1515
|
"v2-i18n-negotiation.test.ts": [
|
|
1513
1516
|
2
|
|
1514
1517
|
],
|
|
@@ -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.46.
|
|
5
|
-
"corpusTag": "v2.46.
|
|
4
|
+
"version": "2.46.24",
|
|
5
|
+
"corpusTag": "v2.46.24",
|
|
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": "ac6de5303872664b78ff419c035915593e01599e02835e777a0123de1baeb398",
|
|
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": "320f8d121cb19c16fe19dbe3ee0783c64951a117af461b6e371c74f1e1607dfa",
|
|
13
|
+
"api/v2/asyncapi.yaml": "51c944d2fb8fcf4169b5cda25bd83378f281e26a8ddda8942eccc6926c55c8a9",
|
|
14
|
+
"api/v2/openapi.yaml": "723c3df7a711a282a1b827fe024cadc365862f49da1f2800e358dac6584360c5",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
|
|
118
118
|
"schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
|
|
119
119
|
"schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
|
|
120
|
-
"schemas/v2/capabilities.schema.json": "
|
|
120
|
+
"schemas/v2/capabilities.schema.json": "6ca940cc38ec7f4b582def52c18337fd1e9ea03d627a5446766230a19af1a8ab",
|
|
121
121
|
"schemas/v2/certification-bundle.schema.json": "5cfa7fb5247160333e33a58b60d42bcd4e5269d8391d6a112d301e14828892ed",
|
|
122
122
|
"schemas/v2/channel-presence-payload.schema.json": "fc03df0c20cc12a25f7ee6a47503a4c5931d79fe3d5ff24815e8fd7c1bcb9a87",
|
|
123
123
|
"schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"schemas/v2/envelopes/schema.request.schema.json": "be2c5f7aa881cdd252ccff47ac02193659e1393633a1541f4297383205d384a2",
|
|
145
145
|
"schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
|
|
146
146
|
"schemas/v2/envelopes/ui.a2ui-surface.schema.json": "e92a2548ba65454c9e0a34f72ea6768d18f59e61fa5da7668d0b2d5baddaddc8",
|
|
147
|
-
"schemas/v2/error-envelope.schema.json": "
|
|
147
|
+
"schemas/v2/error-envelope.schema.json": "f8eff3b5affa29513935632d99ad2d717e76791dc853152d03afa9d907cd5338",
|
|
148
148
|
"schemas/v2/eval-summary.schema.json": "d07f4d116c5410997a81d0bc5387545fb61933a6333c360a26ab8a71c749336b",
|
|
149
149
|
"schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
|
|
150
150
|
"schemas/v2/form-content-pack-manifest.schema.json": "a1942a124e250a6e9fbdb6b811c5a125e78fbcbcaa46dfc71d0636f24710a339",
|
|
@@ -212,14 +212,14 @@
|
|
|
212
212
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
213
213
|
"spec/v1/alias-detectors.json": "22444c22bb88c568339ef512553f9fc38146e22b566d11ab8608f655d688d19c",
|
|
214
214
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
215
|
-
"spec/v1/core-standard-manifest.json": "
|
|
215
|
+
"spec/v1/core-standard-manifest.json": "c2bf78d9853dd41eefebc4e6408feb7abbec4bc52b8e675570512fd5056f20a3",
|
|
216
216
|
"spec/v1/deprecations.json": "e723bc11d7272c71789ad1303bb7f21809a8a024abf57594f9d5c35dc199ece9",
|
|
217
217
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
218
218
|
"spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
|
|
219
219
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
220
220
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
221
221
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
222
|
-
"spec/v1/gaps.json": "
|
|
222
|
+
"spec/v1/gaps.json": "21d53eee36f47b3cd44ba54802acc5cdc810d242d1fd929b1e76167da8566957",
|
|
223
223
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
224
224
|
"spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
|
|
225
225
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -231,8 +231,8 @@
|
|
|
231
231
|
"spec/v2/core/conformance.md": "3ca291e63bc70a6ef829ae8759ad0ded3741ea7aa507bbbdffc57de443ab2ae2",
|
|
232
232
|
"spec/v2/core/connection-packs.md": "c80f95bd6a03698f35c72cdb2b46bd973f49203bbd786c26760e35fa86e6f23d",
|
|
233
233
|
"spec/v2/core/conversation.md": "8d6abedf6af408a74577753bfca24719d318e2632e10b45ff5d63bdcf31a8aaa",
|
|
234
|
-
"spec/v2/core/errors.md": "
|
|
235
|
-
"spec/v2/core/events.md": "
|
|
234
|
+
"spec/v2/core/errors.md": "39845bf7a65f9accaae63f84091eee5978b61bbe35d753e35138a9bbc0a2ad98",
|
|
235
|
+
"spec/v2/core/events.md": "fd1d95f77565b0b0a747544f4f2126bd02ae4f7cf837ead1da4d89e580b7f694",
|
|
236
236
|
"spec/v2/core/execution.md": "4218dbb397534940f83ca5d46561f02bf8f3fdf68d2ae7fefe7f2eb2ed771103",
|
|
237
237
|
"spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
|
|
238
238
|
"spec/v2/core/headers.md": "dae37c2b3980099272e5b9581107c0410d6b23aac95ea6c5a0890c34f0e51c8f",
|
|
@@ -258,10 +258,10 @@
|
|
|
258
258
|
"spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
|
|
259
259
|
"spec/v2/corrections.json": "3a8d93e33865b692d26a3b6bfd3819766e206b2e0c2a69669baa4bf833d05a49",
|
|
260
260
|
"spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
|
|
261
|
-
"spec/v2/declaration.json": "
|
|
261
|
+
"spec/v2/declaration.json": "c914e17b8be18ffd186dbd63556c5a06816562b949029413ba46f556e1cd0d54",
|
|
262
262
|
"spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
|
|
263
263
|
"spec/v2/eos-override.json": "4c72d2f6a658943b01c1de1643cf294642091933af7832f38ee5495df684cb2d",
|
|
264
|
-
"spec/v2/errors.json": "
|
|
264
|
+
"spec/v2/errors.json": "6ae05aec11be85711f207122d2b978a27f1b06cb3ba5a31b6b9b0fc5af4776e5",
|
|
265
265
|
"spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
|
|
266
266
|
"spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
|
|
267
267
|
"spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
|
|
@@ -292,7 +292,7 @@
|
|
|
292
292
|
"spec/v2/facets/connections.schema.json": "f998bb206a5814183bb4720cc34fa43b7ab64d61e39a79437b009c589ce8b78e",
|
|
293
293
|
"spec/v2/facets/envelopeStrictness.schema.json": "28be0a27f880bd45ec1222a16155f5e9167bbca57bf0322970fe1eb2654562aa",
|
|
294
294
|
"spec/v2/facets/heartbeat.schema.json": "81d7fa1895a8b0241bd07cf00a7e7905bef0fd14eb3427325261c62eee5746a6",
|
|
295
|
-
"spec/v2/facets/hostEvents.schema.json": "
|
|
295
|
+
"spec/v2/facets/hostEvents.schema.json": "8834c2fde7bc2ba9eb433f22c9d6ec3f9739f44049eecdbf0a05baf2bfa0bb7d",
|
|
296
296
|
"spec/v2/facets/interrupt.schema.json": "2110ef32c3f880bf706475ef4cf8ddf97ff06871b8e6003a5edbcf4439f1fb70",
|
|
297
297
|
"spec/v2/facets/limits.schema.json": "218fcbd34dd2cdf7548c29c8c76e1a6aea2b7d7df1ba32fbe201bd40917bc66f",
|
|
298
298
|
"spec/v2/facets/mcp.schema.json": "7060657dfcff170f079beed160e3db7c1d3cc4410cd3cf64863baf6459f781cd",
|
|
@@ -307,7 +307,7 @@
|
|
|
307
307
|
"spec/v2/facets/uiPlugins.schema.json": "1fd3f8440cfb6e4422553a7c4b5f091eb3e10a40da0d43e8b977a18929116758",
|
|
308
308
|
"spec/v2/facets/webhooks.schema.json": "54b576f1d150e28e1642693d846ce471c118e9c521bdf3924185305fbd1d0de4",
|
|
309
309
|
"spec/v2/facets/workflowChainPacks.schema.json": "ea625ce818fbeb746478f00f31f96ee9613d538e4409c258c1bfb517acdeaba9",
|
|
310
|
-
"spec/v2/generated/error-codes.md": "
|
|
310
|
+
"spec/v2/generated/error-codes.md": "c024fb18264724cd0d88f33d9b3d509b8fc8c3f16a18f120f2fea069dc04a43d",
|
|
311
311
|
"spec/v2/harness-trust-anchors.json": "6007b92eae16a070fbc3777424efb0abfe865fda89d096963109b96282b91205",
|
|
312
312
|
"spec/v2/harness-trust-anchors.schema.json": "372b5d92337c893d3449eff3f6bfd009ac09fdac285296d8bdbe0a08fff9e34c",
|
|
313
313
|
"spec/v2/id-field-bindings.json": "97e96b6c35c405d0408b5a877c5859714dbbf4507853de9aab381163371ac098",
|
|
@@ -318,9 +318,9 @@
|
|
|
318
318
|
"spec/v2/path-manifest.json": "20f598f56df460ad3ed50aba3d6bc2933ea16a3c96ce125da1c5dd6989d4155c",
|
|
319
319
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
320
320
|
"spec/v2/profiles.json": "b382b261c7844798575f7c1b37c934f0da19846016a2337d883d4f5ef6e05479",
|
|
321
|
-
"spec/v2/release.json": "
|
|
321
|
+
"spec/v2/release.json": "c7708b05e5fdced4ffab715f549f3dc5c9f8947739907eb93f1a2fba401ce0d2",
|
|
322
322
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
323
|
-
"spec/v2/surface-baseline.json": "
|
|
323
|
+
"spec/v2/surface-baseline.json": "e8fa9e1445ee9f65a4448a6daf31386a63a2ed1393ac7cc8aadb874bd8bb7646"
|
|
324
324
|
},
|
|
325
|
-
"corpusCommit": "
|
|
325
|
+
"corpusCommit": "cff9ff17f18b589a3cea7fe96b0d00d751d82c2a"
|
|
326
326
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0250 — pure judges for `v2-host-event-strict-replay.test.ts`. The
|
|
3
|
+
* scenario emits durable and ephemeral host events through the RFC 0241
|
|
4
|
+
* trigger, reconnects with `replay=required`, and records what came back;
|
|
5
|
+
* these functions decide each leg, so the self-tests can fail every wrong
|
|
6
|
+
* host answer without a host.
|
|
7
|
+
*
|
|
8
|
+
* The rules are the `hostEvents.replay` description in
|
|
9
|
+
* `spec/v2/facets/hostEvents.schema.json` (normative text of the `hostEvents`
|
|
10
|
+
* family) and `events.md` §Host events.
|
|
11
|
+
*
|
|
12
|
+
* @see RFCS/0250-opt-in-host-event-replay-and-resync.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readErrorCode } from './error-envelope.js';
|
|
16
|
+
|
|
17
|
+
export interface ReplayFinding { readonly ok: boolean; readonly message: string }
|
|
18
|
+
|
|
19
|
+
/** One SSE frame as the stream delivered it. */
|
|
20
|
+
export interface ReplayFrame { readonly id: string | null; readonly event: string; readonly data: string }
|
|
21
|
+
|
|
22
|
+
/** One HTTP answer to a `replay=required` request. `null`: no answer arrived (a stream that opened and never ended). */
|
|
23
|
+
export type RefusalAnswer = { readonly status: number; readonly contentType: string | null; readonly text: string; readonly json: unknown } | null;
|
|
24
|
+
|
|
25
|
+
const f = (ok: boolean, message: string): ReplayFinding => ({ ok, message });
|
|
26
|
+
export const RESYNC = 'host_event_resync_required';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resume: after cursor `d1`, exactly `after` (in emit order) arrives, never `d1` itself.
|
|
30
|
+
* Frames the suite did not cause (heartbeats, other tests' events) are ignored; only the
|
|
31
|
+
* ids the scenario knows are judged.
|
|
32
|
+
*/
|
|
33
|
+
export function judgeResume(frames: readonly ReplayFrame[], d1: string, after: readonly string[], ephemeral: string): ReplayFinding[] {
|
|
34
|
+
const known = new Set([d1, ephemeral, ...after]);
|
|
35
|
+
const seen = frames.map((x) => x.id).filter((id): id is string => id !== null && known.has(id));
|
|
36
|
+
const durable = seen.filter((id) => id !== ephemeral && id !== d1);
|
|
37
|
+
return [
|
|
38
|
+
f(!seen.includes(d1), `the cursor event MUST NOT be replayed (eventId ${d1} came back)`),
|
|
39
|
+
f(after.every((id) => durable.includes(id)), `every durable event after the cursor MUST be replayed or delivered live (missing: ${after.filter((id) => !durable.includes(id)).join(', ') || 'none'})`),
|
|
40
|
+
f(durable.length === after.length, `each durable event MUST arrive exactly once (got ${durable.length} for ${after.length})`),
|
|
41
|
+
f(JSON.stringify(durable) === JSON.stringify(after), `durable events MUST arrive in commit order, then live (got [${durable.join(', ')}], want [${after.join(', ')}])`),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* No ephemeral event is replayed. An ephemeral frame has no `id:` line (events.md §Host events),
|
|
47
|
+
* so it is found by the `eventId` inside its `data:` envelope, never by `id:`.
|
|
48
|
+
*/
|
|
49
|
+
export function judgeNoEphemeralReplay(frames: readonly ReplayFrame[], ephemeral: string): ReplayFinding[] {
|
|
50
|
+
return [f(!frames.some((x) => x.id === ephemeral || x.data.includes(ephemeral)), `an ephemeral event MUST NOT be replayed (eventId ${ephemeral} came back)`)];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** An unresumable cursor is refused 409 host_event_resync_required, before any SSE byte. */
|
|
54
|
+
export function judgeRefused(a: RefusalAnswer, what: string): ReplayFinding[] {
|
|
55
|
+
if (a === null) return [f(false, `${what}: the host opened a stream instead of refusing — a cursor it cannot resume MUST be refused 409 before any SSE byte`)];
|
|
56
|
+
return [
|
|
57
|
+
f(a.status === 409, `${what}: MUST be refused 409 (got ${a.status})`),
|
|
58
|
+
f(readErrorCode(a.json) === RESYNC, `${what}: the refusal MUST carry ${RESYNC} (got ${String(readErrorCode(a.json))})`),
|
|
59
|
+
f(!(a.contentType ?? '').includes('text/event-stream'), `${what}: the refusal MUST come before any SSE byte (content-type ${a.contentType ?? 'none'})`),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An unauthenticated `replay=required` naming a real cursor is never resumed: the host answers
|
|
65
|
+
* its authentication failure (401) or the uniform refusal (409), never a stream of that tenant's
|
|
66
|
+
* events. Found by openwop-app (ADR 0973): an unauthenticated caller resolved to a default tenant.
|
|
67
|
+
*/
|
|
68
|
+
export function judgeUnauthenticatedNotResumed(a: RefusalAnswer): ReplayFinding[] {
|
|
69
|
+
if (a === null) return [f(false, 'an unauthenticated replay=required with a real cursor opened a stream — it MUST be refused (401, or the uniform 409)')];
|
|
70
|
+
return [f((a.status === 401 || a.status === 409) && !(a.contentType ?? '').includes('text/event-stream'), `an unauthenticated replay=required with a real cursor MUST be refused 401 or 409, never resumed (got ${a.status})`)];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Every refusal is byte-identical, so a refusal is not an oracle for another tenant's events. */
|
|
74
|
+
export function judgeUniform(answers: ReadonlyArray<{ what: string; a: RefusalAnswer }>): ReplayFinding[] {
|
|
75
|
+
const base = answers[0];
|
|
76
|
+
if (base === undefined) return [f(false, 'no refusal was recorded')];
|
|
77
|
+
return answers.slice(1).map(({ what, a }) => f(
|
|
78
|
+
a !== null && base.a !== null && a.status === base.a.status && a.text === base.a.text,
|
|
79
|
+
`${what} MUST be answered byte-identically to ${base.what} (status ${a?.status ?? 'stream'} vs ${base.a?.status ?? 'stream'}; bodies ${a !== null && base.a !== null && a.text === base.a.text ? 'equal' : 'differ'})`,
|
|
80
|
+
));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Without `replay=required`, an unknown cursor keeps its old answer: not the new refusal. */
|
|
84
|
+
export function judgeLegacy(status: number | null): ReplayFinding[] {
|
|
85
|
+
return [f(status !== null && status !== 409, `without replay=required an unknown Last-Event-ID MUST keep its existing behaviour, not ${RESYNC} (got ${status ?? 'no answer'})`)];
|
|
86
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — opt-in strict host-event replay (RFC 0250; the rules are the
|
|
3
|
+
* `hostEvents.replay` description in `spec/v2/facets/hostEvents.schema.json`
|
|
4
|
+
* and `events.md` §Host events).
|
|
5
|
+
*
|
|
6
|
+
* Server-free (always on):
|
|
7
|
+
* 1. `0250.shape` — the `replay` facet member validates; a zero retention,
|
|
8
|
+
* an unknown member and the old `mode` member are refused.
|
|
9
|
+
*
|
|
10
|
+
* Host legs — gated on `hostEvents.replay` and the RFC 0241 trigger
|
|
11
|
+
* (`host-test.durable-triggered` and `host-test.ephemeral-triggered` listed).
|
|
12
|
+
* One sequence per worker: emit durable D1, D2, ephemeral E, durable D3 with
|
|
13
|
+
* no stream open, then reconnect with `replay=required` and `Last-Event-ID: D1`
|
|
14
|
+
* and emit D4 while the stream is open. Judges in
|
|
15
|
+
* `lib/host-event-replay-witness.ts`.
|
|
16
|
+
* 2. `0250.resume-backlog` — exactly D2, D3, D4 arrive, in that order, never
|
|
17
|
+
* D1: the backlog, then live, with no gap at the handoff.
|
|
18
|
+
* 3. `0250.ephemeral-not-replayed` — E does not come back.
|
|
19
|
+
* 4. `0250.cursor-refused` — an unknown cursor is `409
|
|
20
|
+
* host_event_resync_required` before any SSE byte.
|
|
21
|
+
* 5. `0250.refusal-uniform` — E's id and a second tenant's real durable id
|
|
22
|
+
* (`OPENWOP_TEST_TENANT_B_API_KEY`) are refused byte-identically to the
|
|
23
|
+
* unknown cursor, so a refusal is no oracle; and an unauthenticated
|
|
24
|
+
* request naming D1 is refused (401 or the uniform 409), never resumed.
|
|
25
|
+
* 6. `0250.legacy-unchanged` — without `replay=required`, an unknown cursor
|
|
26
|
+
* is not answered with the new refusal.
|
|
27
|
+
*
|
|
28
|
+
* Retention across process replacement and expiry are not caused here: losing
|
|
29
|
+
* an event inside the retention window is a violation of leg 2 however it
|
|
30
|
+
* happens, and the suite cannot replace a host's process.
|
|
31
|
+
*
|
|
32
|
+
* @see RFCS/0250-opt-in-host-event-replay-and-resync.md
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { describe, it, expect } from 'vitest';
|
|
36
|
+
import { randomBytes } from 'node:crypto';
|
|
37
|
+
import { driver } from '../lib/driver.js';
|
|
38
|
+
import { v2Discovery, familyAdvertised, v2RefValidator } from '../lib/v2.js';
|
|
39
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
40
|
+
import { req } from '../lib/requirement-ids.js';
|
|
41
|
+
import { subscribe } from '../lib/sse.js';
|
|
42
|
+
import { advertisedTypes, testTypeListed, TEST_TYPES } from '../lib/host-event-witness.js';
|
|
43
|
+
import {
|
|
44
|
+
judgeLegacy, judgeNoEphemeralReplay, judgeRefused, judgeUnauthenticatedNotResumed, judgeResume, judgeUniform,
|
|
45
|
+
type RefusalAnswer, type ReplayFinding, type ReplayFrame,
|
|
46
|
+
} from '../lib/host-event-replay-witness.js';
|
|
47
|
+
|
|
48
|
+
const DOC = 'hostEvents.schema.json §replay, events.md §Host events (RFC 0250)';
|
|
49
|
+
const ID_SHAPE = 'openwop.requirement.0250.shape';
|
|
50
|
+
const ID_RESUME = 'openwop.requirement.0250.resume-backlog';
|
|
51
|
+
const ID_EPHEMERAL = 'openwop.requirement.0250.ephemeral-not-replayed';
|
|
52
|
+
const ID_REFUSED = 'openwop.requirement.0250.cursor-refused';
|
|
53
|
+
const ID_UNIFORM = 'openwop.requirement.0250.refusal-uniform';
|
|
54
|
+
const ID_LEGACY = 'openwop.requirement.0250.legacy-unchanged';
|
|
55
|
+
|
|
56
|
+
const TRIGGER = '/host/events/test';
|
|
57
|
+
const V2 = { 'OpenWOP-Version': '2.0' };
|
|
58
|
+
const STREAM_MS = 4_000;
|
|
59
|
+
const REFUSAL_MS = 4_000;
|
|
60
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
61
|
+
|
|
62
|
+
type Skip = { skip: [Parameters<typeof softSkip>[0], string] };
|
|
63
|
+
async function gate(): Promise<Skip | { ok: true }> {
|
|
64
|
+
let doc: Record<string, unknown> | null;
|
|
65
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
66
|
+
if (!doc) return { skip: ['blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0'] };
|
|
67
|
+
const record = await familyAdvertised('hostEvents');
|
|
68
|
+
if (record === null) return { skip: ['inapplicable', 'the host does not advertise hostEvents'] };
|
|
69
|
+
const replay = record['replay'];
|
|
70
|
+
if (replay === null || typeof replay !== 'object') return { skip: ['inapplicable', 'the host does not advertise hostEvents.replay'] };
|
|
71
|
+
const types = advertisedTypes(record);
|
|
72
|
+
if (testTypeListed(types, 'durable') === null || testTypeListed(types, 'ephemeral') === null) {
|
|
73
|
+
return { skip: ['inapplicable', `hostEvents.types lists ${TEST_TYPES.durable} and ${TEST_TYPES.ephemeral} not both, so the suite cannot cause the sequence (RFC 0241 §B.1)`] };
|
|
74
|
+
}
|
|
75
|
+
return { ok: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function emit(delivery: 'durable' | 'ephemeral', bearer?: string): Promise<string | null> {
|
|
79
|
+
try {
|
|
80
|
+
const res = await driver.post(TRIGGER, { delivery }, bearer === undefined ? {} : { authenticated: false, headers: { Authorization: `Bearer ${bearer}` } });
|
|
81
|
+
const id = (res.json as { eventId?: unknown } | undefined)?.eventId;
|
|
82
|
+
return res.status === 202 && typeof id === 'string' ? id : null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A `replay=required` request expected to be refused; `null` when a stream opened instead. */
|
|
89
|
+
async function refusalFor(cursor: string, anonymous = false): Promise<RefusalAnswer> {
|
|
90
|
+
try {
|
|
91
|
+
const r = await driver.get('/host/events?replay=required', { headers: { ...V2, 'Last-Event-ID': cursor }, timeoutMs: REFUSAL_MS, ...(anonymous ? { authenticated: false } : {}) });
|
|
92
|
+
return { status: r.status, contentType: r.headers.get('content-type'), text: r.text, json: r.json };
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface Sequence {
|
|
99
|
+
readonly blockedAt?: string;
|
|
100
|
+
readonly d1?: string; readonly after?: string[]; readonly e?: string;
|
|
101
|
+
readonly frames?: ReplayFrame[];
|
|
102
|
+
}
|
|
103
|
+
let seq: Promise<Sequence> | undefined;
|
|
104
|
+
function sequence(): Promise<Sequence> {
|
|
105
|
+
seq ??= drive();
|
|
106
|
+
return seq;
|
|
107
|
+
}
|
|
108
|
+
async function drive(): Promise<Sequence> {
|
|
109
|
+
const d1 = await emit('durable'); if (d1 === null) return { blockedAt: `${TRIGGER} did not accept durable D1` };
|
|
110
|
+
const d2 = await emit('durable'); if (d2 === null) return { blockedAt: `${TRIGGER} did not accept durable D2` };
|
|
111
|
+
const e = await emit('ephemeral'); if (e === null) return { blockedAt: `${TRIGGER} did not accept ephemeral E` };
|
|
112
|
+
const d3 = await emit('durable'); if (d3 === null) return { blockedAt: `${TRIGGER} did not accept durable D3` };
|
|
113
|
+
const sub = subscribe('/host/events?replay=required', { timeoutMs: STREAM_MS, extraHeaders: V2, lastEventId: d1 });
|
|
114
|
+
await sleep(800);
|
|
115
|
+
const d4 = await emit('durable');
|
|
116
|
+
const res = await sub;
|
|
117
|
+
if (d4 === null) return { blockedAt: `${TRIGGER} did not accept durable D4 while the resumed stream was open` };
|
|
118
|
+
if (res.status !== 200) return { blockedAt: `the replay=required stream with cursor D1 answered ${res.status}` };
|
|
119
|
+
return { d1, after: [d2, d3, d4], e, frames: res.events.map((x) => ({ id: x.id, event: x.event, data: x.data })) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const expectAll = (id: string, findings: readonly ReplayFinding[]): void => {
|
|
123
|
+
for (const x of findings) expect(x.ok, req(id, DOC, x.message)).toBe(true);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
describe('v2 RFC 0250 — hostEvents.replay shape (server-free)', () => {
|
|
127
|
+
it('the replay facet member validates, and its negatives are refused', () => {
|
|
128
|
+
const v = v2RefValidator('capabilities.schema.json#/properties/hostEvents/properties/replay');
|
|
129
|
+
const S = 'hostEvents.schema.json §replay (RFC 0250)';
|
|
130
|
+
expect(v({ minRetentionSeconds: 3600 }).ok, req(ID_SHAPE, S, 'a positive retention MUST validate')).toBe(true);
|
|
131
|
+
expect(v({ minRetentionSeconds: 0 }).ok, req(ID_SHAPE, S, 'a zero retention MUST be refused')).toBe(false);
|
|
132
|
+
expect(v({}).ok, req(ID_SHAPE, S, 'minRetentionSeconds is required')).toBe(false);
|
|
133
|
+
expect(v({ minRetentionSeconds: 60, mode: 'required-cursor-v1' }).ok, req(ID_SHAPE, S, 'the member is closed (the Draft mode const was dropped)')).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('v2 RFC 0250 — strict replay (host)', () => {
|
|
138
|
+
it('a resumed stream delivers the backlog after the cursor, then live, in order', async () => {
|
|
139
|
+
const g = await gate();
|
|
140
|
+
if ('skip' in g) return softSkip(...g.skip);
|
|
141
|
+
const s = await sequence();
|
|
142
|
+
if (s.frames === undefined) return softSkip('blocked', s.blockedAt ?? 'the sequence did not complete');
|
|
143
|
+
expectAll(ID_RESUME, judgeResume(s.frames, s.d1!, s.after!, s.e!));
|
|
144
|
+
}, 30_000);
|
|
145
|
+
|
|
146
|
+
it('an ephemeral event is not replayed', async () => {
|
|
147
|
+
const g = await gate();
|
|
148
|
+
if ('skip' in g) return softSkip(...g.skip);
|
|
149
|
+
const s = await sequence();
|
|
150
|
+
if (s.frames === undefined) return softSkip('blocked', s.blockedAt ?? 'the sequence did not complete');
|
|
151
|
+
expectAll(ID_EPHEMERAL, judgeNoEphemeralReplay(s.frames, s.e!));
|
|
152
|
+
}, 30_000);
|
|
153
|
+
|
|
154
|
+
it('an unknown cursor is refused 409 host_event_resync_required before any SSE byte', async () => {
|
|
155
|
+
const g = await gate();
|
|
156
|
+
if ('skip' in g) return softSkip(...g.skip);
|
|
157
|
+
expectAll(ID_REFUSED, judgeRefused(await refusalFor(`conformance-unknown-${randomBytes(8).toString('hex')}`), 'an unknown cursor'));
|
|
158
|
+
}, 15_000);
|
|
159
|
+
|
|
160
|
+
it('a foreign or ephemeral cursor is refused byte-identically to an unknown one', async () => {
|
|
161
|
+
const g = await gate();
|
|
162
|
+
if ('skip' in g) return softSkip(...g.skip);
|
|
163
|
+
const tenantB = process.env['OPENWOP_TEST_TENANT_B_API_KEY']?.trim();
|
|
164
|
+
if (!tenantB) return softSkip('blocked', 'OPENWOP_TEST_TENANT_B_API_KEY (a credential bound to a second tenant) is not set');
|
|
165
|
+
if (tenantB === process.env['OPENWOP_API_KEY']) return softSkip('blocked', 'OPENWOP_TEST_TENANT_B_API_KEY equals OPENWOP_API_KEY — the leg needs a second tenant');
|
|
166
|
+
const s = await sequence();
|
|
167
|
+
if (s.e === undefined) return softSkip('blocked', s.blockedAt ?? 'the sequence did not complete');
|
|
168
|
+
const foreign = await emit('durable', tenantB);
|
|
169
|
+
if (foreign === null) return softSkip('blocked', `${TRIGGER} as tenant B did not accept a durable event`);
|
|
170
|
+
const unknown = await refusalFor(`conformance-unknown-${randomBytes(8).toString('hex')}`);
|
|
171
|
+
const answers = [
|
|
172
|
+
{ what: 'an unknown cursor', a: unknown },
|
|
173
|
+
{ what: "another tenant's real durable cursor", a: await refusalFor(foreign) },
|
|
174
|
+
{ what: "an ephemeral event's id", a: await refusalFor(s.e) },
|
|
175
|
+
];
|
|
176
|
+
const anonymous = await refusalFor(s.d1!, true);
|
|
177
|
+
expectAll(ID_UNIFORM, [...answers.flatMap(({ what, a }) => judgeRefused(a, what)), ...judgeUniform(answers), ...judgeUnauthenticatedNotResumed(anonymous)]);
|
|
178
|
+
}, 30_000);
|
|
179
|
+
|
|
180
|
+
it('without replay=required an unknown cursor keeps its existing answer', async () => {
|
|
181
|
+
const g = await gate();
|
|
182
|
+
if ('skip' in g) return softSkip(...g.skip);
|
|
183
|
+
const r = await subscribe('/host/events', { timeoutMs: 1_500, extraHeaders: V2, lastEventId: `conformance-unknown-${randomBytes(8).toString('hex')}` });
|
|
184
|
+
expectAll(ID_LEGACY, judgeLegacy(r.status === 0 ? null : r.status));
|
|
185
|
+
}, 15_000);
|
|
186
|
+
});
|