@openwop/openwop-conformance 2.0.0-rc.42 → 2.0.0-rc.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
|
|
|
112
112
|
|
|
113
113
|
## What's Covered
|
|
114
114
|
|
|
115
|
-
The current suite has
|
|
115
|
+
The current suite has 504 scenario files under `src/scenarios/`.
|
|
116
116
|
- 2026-09-03 (suite `1.157.0 -> 1.158.0`, gap G17): NEW `idempotency-concurrent-claim.test.ts` — drives the new `host-sample-test-seams.md` §25 concurrent duplicate-delivery seam for the RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" atomic-claim MUST, which is unconditional and had no witness of any kind. Asserts every executor mints the SAME `logicalInvocationId` **before** asserting `delivered === 1` — without the identity check a host passes by minting different ids and never colliding, one effect because nothing raced. Not profile-gated and so not opt-out-able (the obligation is unconditional); an unmounted seam records `blocked`, which is not certifiable. Graduates `layer2-invocation-claim-atomic` reference-impl -> protocol.
|
|
117
117
|
- 2026-08-19 (suite `1.137.0 → 1.138.0`): NEW `durability-poison-exhaustion.test.ts` — RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts what `failure-path.test.ts` cannot: not just that deterministically failing work reaches terminal, but that attempts STOP — counted on the log, re-counted after a scaled quiet window, asserted unchanged. A host still redelivering records more. Seam-gated on the existing event-log seam (`blocked` = unobservable, not unmet) and outside every profile floor.
|
|
118
118
|
- 2026-08-19 (suite `1.136.15 → 1.137.0`): NEW `replay-fanout-suppression.test.ts` — capability-gated on `webhooks.supported`, **outside every profile floor**; witnesses `replay.md` §"Host-initiated fan-out is an external effect", which was the largest normative MUST NOT on the replay surface with no scenario and no SECURITY invariant. Three legs in ONE `it` against ONE receiver and ONE subscription — a positive control, the MUST NOT, and a `branch` boundary leg — because "no delivery arrived" passes identically when delivery never worked, so absence is asserted only after presence is proven on that exact wiring. A host with an SSRF guard correctly refuses the loopback receiver and records `blocked`: **unobservable, not unmet.**
|
|
@@ -457,7 +457,7 @@ Server-required (added in 1.7.0):
|
|
|
457
457
|
| ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
458
458
|
| **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
|
|
459
459
|
|
|
460
|
-
Current source tree:
|
|
460
|
+
Current source tree: 504 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
|
|
461
461
|
|
|
462
462
|
## Remaining Gaps
|
|
463
463
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@openwop/spec-artifacts",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
4
|
-
"stampSha256": "
|
|
3
|
+
"version": "2.0.0-rc.44",
|
|
4
|
+
"stampSha256": "ce4913c7555550d9aa319f3e3caa2a9deeb3c44cacfb040ea5d91c0719d65f9f"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.44",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.0.0-rc.
|
|
59
|
+
"@openwop/spec-artifacts": "2.0.0-rc.44"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
|
-
"files":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 551,
|
|
6
|
+
"tests": 2127,
|
|
7
|
+
"withStableId": 2126,
|
|
8
8
|
"interpolatedTitles": 1,
|
|
9
9
|
"explicitIds": 2057
|
|
10
10
|
},
|
|
@@ -26311,6 +26311,40 @@
|
|
|
26311
26311
|
}
|
|
26312
26312
|
]
|
|
26313
26313
|
},
|
|
26314
|
+
{
|
|
26315
|
+
"id": "openwop.it.v2-created-run-readable.a-run-created-at-this-base-is-readable-and-pollable-at-this-base-by-the-id-it-re",
|
|
26316
|
+
"file": "v2-created-run-readable.test.ts",
|
|
26317
|
+
"line": 63,
|
|
26318
|
+
"title": "a run created at this base is readable and pollable at this base by the id it returned, and its links stay on this base",
|
|
26319
|
+
"explicitId": null,
|
|
26320
|
+
"citations": [
|
|
26321
|
+
{
|
|
26322
|
+
"section": "spec/v2/core/identity.md §5",
|
|
26323
|
+
"requirement": null,
|
|
26324
|
+
"interpolated": true
|
|
26325
|
+
},
|
|
26326
|
+
{
|
|
26327
|
+
"section": null,
|
|
26328
|
+
"requirement": null,
|
|
26329
|
+
"interpolated": true
|
|
26330
|
+
},
|
|
26331
|
+
{
|
|
26332
|
+
"section": "spec/v2/core/events.md §Poll",
|
|
26333
|
+
"requirement": null,
|
|
26334
|
+
"interpolated": true
|
|
26335
|
+
},
|
|
26336
|
+
{
|
|
26337
|
+
"section": null,
|
|
26338
|
+
"requirement": null,
|
|
26339
|
+
"interpolated": true
|
|
26340
|
+
},
|
|
26341
|
+
{
|
|
26342
|
+
"section": null,
|
|
26343
|
+
"requirement": null,
|
|
26344
|
+
"interpolated": true
|
|
26345
|
+
}
|
|
26346
|
+
]
|
|
26347
|
+
},
|
|
26314
26348
|
{
|
|
26315
26349
|
"id": "openwop.it.v2-dual-stack-negotiation.a-run-created-through-v1-runs-with-no-header-is-readable-through-runs-under-open",
|
|
26316
26350
|
"file": "v2-dual-stack-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": 504,
|
|
5
5
|
"v1": 445,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 60
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1264,6 +1264,9 @@
|
|
|
1264
1264
|
"v2-configurable-closed.test.ts": [
|
|
1265
1265
|
2
|
|
1266
1266
|
],
|
|
1267
|
+
"v2-created-run-readable.test.ts": [
|
|
1268
|
+
2
|
|
1269
|
+
],
|
|
1267
1270
|
"v2-dual-stack-negotiation.test.ts": [
|
|
1268
1271
|
2
|
|
1269
1272
|
],
|
|
@@ -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.44",
|
|
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": "8e5c8fdba4f31a27b5b035df292de75278285db89b0c18b282d61095c2a13fbb",
|
|
13
|
+
"api/v2/asyncapi.yaml": "69715abf7a6d0bef193043235fc9df02228f0c55072ce10095677108ea249b24",
|
|
14
|
+
"api/v2/openapi.yaml": "6e75d0561b8e7f82fb6641b17b1e0eb247b9078d5ed21f8dbce0fea36af402a5",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
"schemas/v2/envelopes/schema.request.schema.json": "be2c5f7aa881cdd252ccff47ac02193659e1393633a1541f4297383205d384a2",
|
|
142
142
|
"schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
|
|
143
143
|
"schemas/v2/envelopes/ui.a2ui-surface.schema.json": "0a06fbbdc8ba797a6818c79336a98e211ac5ba5747945b4e4098072cd3974d79",
|
|
144
|
-
"schemas/v2/error-envelope.schema.json": "
|
|
144
|
+
"schemas/v2/error-envelope.schema.json": "c08241b74e41f4e3524c25f1c611195ed0e79b49a8ecf63e304add67070bdce5",
|
|
145
145
|
"schemas/v2/eval-summary.schema.json": "bcb165148531d33b7dc01de83da3a049d19ebbef6d12300b17a234c7b03d77d9",
|
|
146
146
|
"schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
|
|
147
147
|
"schemas/v2/form-content-pack-manifest.schema.json": "307884e57946703b6f86ec283034bf56beb68bea4a000743ebbc9eebeed31b07",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
200
200
|
"spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
|
|
201
201
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
202
|
-
"spec/v1/core-standard-manifest.json": "
|
|
202
|
+
"spec/v1/core-standard-manifest.json": "1c7d01a8c22a9d071849726974dcc62b15ef492bb4ce7e12748466340a4a0fa9",
|
|
203
203
|
"spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
|
|
204
204
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
205
205
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"spec/v1/spec-gaps.json": "6cc9962c6b969f632e07a78b52a4f61447ff579e2990cbae989866f584a86042",
|
|
214
214
|
"spec/v2/declaration.json": "52b8c24a2dd463d44086129bf792ad108746f5102778cbbf0ff469fa534eca48",
|
|
215
215
|
"spec/v2/declaration.schema.json": "b0881a76228dfbd1b85a2ec7b7259a60197f768344535cdb475db6ff719b0e54",
|
|
216
|
-
"spec/v2/errors.json": "
|
|
216
|
+
"spec/v2/errors.json": "c2dd3fa0401efa0dc78da3b21e32b58a89ed5114cebdde96d3eb999a702118f0",
|
|
217
217
|
"spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
|
|
218
218
|
"spec/v2/event-codemap.schema.json": "ce2d0ca085d7668d7ef745d2560b385ec6881d24a6d4b47c49931986b2ba642d",
|
|
219
219
|
"spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
"spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
|
|
231
231
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
232
232
|
"spec/v2/profiles.json": "1f0ee40491131cf561ef19a0394c47ba476b4567514fe16befb6cada4a10a5a4",
|
|
233
|
-
"spec/v2/release.json": "
|
|
233
|
+
"spec/v2/release.json": "d21d0f15969459d1017987918c64694f92b0ccff3185a46b1626b435cab48022"
|
|
234
234
|
},
|
|
235
|
-
"corpusCommit": "
|
|
235
|
+
"corpusCommit": "cc7bb617f49000f7ec9c94eef0fe0d18a2a6901a"
|
|
236
236
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec/v2/core/runs.md` §Create + `versioning.md` §1.2 — a run created at a
|
|
3
|
+
* base is readable at that base by the id the base returned, and the links in
|
|
4
|
+
* the create response stay on that base (suite 2.0.0, target major 2; unaided;
|
|
5
|
+
* creates one run).
|
|
6
|
+
*
|
|
7
|
+
* This is the assertion the suite deferred to a soft-skip. `v2-id-grammar`
|
|
8
|
+
* reads the run it just created and, on a non-200, records `blocked` — the
|
|
9
|
+
* honest word for "the host did not let me look". It is the wrong word when
|
|
10
|
+
* the host DID answer and the answer was `404 No route matches this request`,
|
|
11
|
+
* because that is not evidence going unread; it is a routing hole, and a
|
|
12
|
+
* `blocked` row over a routing hole is a vacuous pass one step removed.
|
|
13
|
+
*
|
|
14
|
+
* Measured 2026-09-05 on a tier-1 host through its public origin: a run
|
|
15
|
+
* created at `https://app.openwop.dev` under major 2 could not be read,
|
|
16
|
+
* polled, cancelled or streamed at `https://app.openwop.dev` — the hosting
|
|
17
|
+
* layer decoded the tenant-bound id's `%2F` to `/` before forwarding, and the
|
|
18
|
+
* backend correctly had no route for a literal slash. One hop behind, the
|
|
19
|
+
* direct service URL answered every read with 200. Every bound id was
|
|
20
|
+
* unreachable through the front door, and every bound-id scenario recorded
|
|
21
|
+
* `blocked`. The same create response linked `eventsUrl` and `statusUrl` to
|
|
22
|
+
* `http://<the direct service host>/…` — a client that follows them leaves the
|
|
23
|
+
* origin it discovered on and downgrades to plain http.
|
|
24
|
+
*
|
|
25
|
+
* Two hard assertions, then: the read-back MUST be 200 at the same base, and
|
|
26
|
+
* the links MUST resolve under the request's origin without a scheme
|
|
27
|
+
* downgrade — a relative path satisfies both.
|
|
28
|
+
*
|
|
29
|
+
* @see spec/v2/core/runs.md §Create
|
|
30
|
+
* @see spec/v2/core/versioning.md §1.2
|
|
31
|
+
* @see spec/v2/core/identity.md §5
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { describe, it, expect } from 'vitest';
|
|
35
|
+
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
36
|
+
import { loadEnv } from '../lib/env.js';
|
|
37
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
38
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
39
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
40
|
+
import { req } from '../lib/requirement-ids.js';
|
|
41
|
+
|
|
42
|
+
const ID = 'openwop.requirement.0172.created-run-readable';
|
|
43
|
+
const DOC = 'spec/v2/core/runs.md §Create';
|
|
44
|
+
const NOOP_WORKFLOW_ID = 'conformance-noop';
|
|
45
|
+
const RUN_ID = /^[A-Za-z0-9._~-]{1,128}\/[A-Za-z0-9._~-]{16,128}$/;
|
|
46
|
+
|
|
47
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
|
|
48
|
+
try { return await fn(); } catch { return null; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A link is on-base when it is relative, or absolute under the request origin with the same scheme. */
|
|
52
|
+
function linkOnBase(link: unknown, baseUrl: string): { ok: boolean; why: string } {
|
|
53
|
+
if (typeof link !== 'string' || link.length === 0) return { ok: false, why: 'not a non-empty string' };
|
|
54
|
+
if (link.startsWith('/')) return { ok: true, why: 'relative' };
|
|
55
|
+
let l: URL; let b: URL;
|
|
56
|
+
try { l = new URL(link); b = new URL(baseUrl); } catch { return { ok: false, why: 'not a URL' }; }
|
|
57
|
+
if (l.protocol === 'http:' && b.protocol === 'https:') return { ok: false, why: `scheme downgrade ${b.protocol} → ${l.protocol}` };
|
|
58
|
+
if (l.origin !== b.origin) return { ok: false, why: `origin ${l.origin} is not the request origin ${b.origin}` };
|
|
59
|
+
return { ok: true, why: 'same-origin absolute' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('v2 created-run-readable (runs.md §Create)', () => {
|
|
63
|
+
it('a run created at this base is readable and pollable at this base by the id it returned, and its links stay on this base', async () => {
|
|
64
|
+
try { if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable'); } catch { return softSkip('blocked', 'v2 discovery unreachable'); }
|
|
65
|
+
const { baseUrl } = loadEnv();
|
|
66
|
+
const created = await http(() => driver.post('/runs', { workflowId: NOOP_WORKFLOW_ID }));
|
|
67
|
+
if (created === null) return softSkip('blocked', 'POST /runs unreachable (fetch failed)');
|
|
68
|
+
if (created.status === 429) return softSkip('blocked', 'POST /runs answered 429 — the run budget, not the wire');
|
|
69
|
+
const body = (created.json ?? {}) as { runId?: unknown; eventsUrl?: unknown; statusUrl?: unknown };
|
|
70
|
+
if (created.status !== 201 || typeof body.runId !== 'string') return softSkip('blocked', `POST /runs {workflowId: ${NOOP_WORKFLOW_ID}} answered ${created.status} ${readErrorCode(created.json) ?? ''} — the smallest valid create was refused (fixture not seeded?)`.trim());
|
|
71
|
+
const runId = body.runId;
|
|
72
|
+
expect(RUN_ID.test(runId), req(ID, 'spec/v2/core/identity.md §5', `the created runId MUST be tenant-bound (got ${runId})`)).toBe(true);
|
|
73
|
+
|
|
74
|
+
// HARD, not soft: the base that minted the id MUST resolve it. A 404 here is
|
|
75
|
+
// not evidence going unread; it is the front door not routing its own ids.
|
|
76
|
+
const read = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}`));
|
|
77
|
+
expect(
|
|
78
|
+
read?.status ?? null,
|
|
79
|
+
req(ID, DOC, `GET /runs/{runId} at the base that created the run MUST answer 200 by the id it returned — got ${read?.status ?? 'no response'} ${readErrorCode(read?.json) ?? ''}. A 404 "No route matches" for a tenant-bound id is a hosting layer decoding %2F to / before the backend sees the path: every bound id is unreachable through this base`.trim()),
|
|
80
|
+
).toBe(200);
|
|
81
|
+
const poll = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`));
|
|
82
|
+
expect(
|
|
83
|
+
poll?.status ?? null,
|
|
84
|
+
req(ID, 'spec/v2/core/events.md §Poll', `GET /runs/{runId}/events/poll at the base that created the run MUST answer 200 — got ${poll?.status ?? 'no response'} ${readErrorCode(poll?.json) ?? ''}`.trim()),
|
|
85
|
+
).toBe(200);
|
|
86
|
+
|
|
87
|
+
// The links in the create response MUST keep the client on this base.
|
|
88
|
+
const ev = linkOnBase(body.eventsUrl, baseUrl);
|
|
89
|
+
expect(
|
|
90
|
+
ev.ok,
|
|
91
|
+
req(ID, DOC, `eventsUrl MUST resolve under the origin the create was made to, without a scheme downgrade — a relative path satisfies both (got ${JSON.stringify(body.eventsUrl)}: ${ev.why}). A link to a different host leaves the origin the client discovered on; http on an https origin is a downgrade`),
|
|
92
|
+
).toBe(true);
|
|
93
|
+
if (body.statusUrl !== undefined) {
|
|
94
|
+
const st = linkOnBase(body.statusUrl, baseUrl);
|
|
95
|
+
expect(st.ok, req(ID, DOC, `statusUrl, when present, MUST resolve under the request origin without a scheme downgrade (got ${JSON.stringify(body.statusUrl)}: ${st.why})`)).toBe(true);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|