@openwop/openwop-conformance 2.39.4 → 2.39.5
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 +8 -0
- package/README.md +1 -1
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +37 -5
- package/schemas/CORPUS-STAMP.json +10 -10
- package/src/lib/host-public-origin.ts +22 -2
- package/src/scenarios/v2-idempotency-in-flight.test.ts +31 -0
- package/src/scenarios/v2-revocation-honored.test.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.39.5] — 2026-09-26 — RFC 0213 §B has a deterministic witness, and a host front whose URLs follow the request origin verifies
|
|
4
|
+
|
|
5
|
+
- **RFC 0213 §B has a deterministic witness: `0213.in-flight-refused-under-hold`.** On a host whose create answers in milliseconds the unaided `v2-idempotency-in-flight` leg records `partial-witness` — same-key creates never overlap. The new leg arms the seams-profile seam `armIdempotencyHold` (`POST /conformance/seams/sample/test/idempotency/hold`, host-sample-test-seams.md §26), which holds the caller's next real create's claim in flight; a concurrent same-key create MUST then be refused `409 idempotency_in_flight` by the host's own path (no retry timing in `details`), and the held create still wins. Seams not advertised ⇒ `inapplicable`; advertised but the path unserved ⇒ `blocked`.
|
|
6
|
+
- **`OPENWOP_HOST_PUBLIC_URL` verifies a host whose advertised URLs follow the request origin (fixes #1567).** 2.39.4 required the discovery document fetched through the declared front to be byte-identical to the one fetched over `--base-url`. A host that derives `agentCardUrl`, MCP `serverUrls` and similar values from the REQUEST origin embeds the loopback origin in one and the front's origin in the other, so the check could never pass on exactly the hosts it exists for. On the v2-reference public cut on suite 2.39.4 it was the only blocked row (`0199.credential-interrupt`), and it denied certification of an otherwise 397-pass / 0-fail bundle (openwop-1f). Both origins are now replaced by one placeholder in both documents before the structural comparison, because a host with a public base may embed either origin in either response. Everything else must still match, so a front serving another host is still refused. Checked against real v2-reference discovery fetched both ways (a local boot with a public base); two new self-tests cover the measured shape, and removing the normalisation fails the regression test.
|
|
7
|
+
- **The RFC 0170 credential mint/revoke seams are in the seams contract.** `v2-revocation-honored` drives `POST …/auth/credential/mint` and `…/auth/credential/revoke`. RFC 0170 §B.3 names them, and the v2 reference host has served them. Until now neither `api/seams-v2.yaml` nor `host-sample-test-seams.md` defined them, so a host advertising `openwop-conformance-seams-v2` could not know it owed them, and under `--require-behavior` their absence was a hard fail. openwop-app's seams-on in-memory boot hit exactly that on 2026-09-25. Added as `mintLaneCredential` / `revokeLaneCredential`, in the shape the reference host already serves (not a new shape), with the prose bullet beside the RFC 0199 seams. The scenario's `seamAbsent` message now names the contract instead of calling the seam unspecified. No committed bundle changes: MyndHyve's and openwop-app's do not advertise the seams profile (the row is `inapplicable`), and v2-reference's passes it.
|
|
8
|
+
- **RFC 0215 gaps G3 and G6 close in the contract peer.** `@openwop/spec-artifacts`' `spec/v1/gaps.json` records the prior-art survey and the threat-model home (`threat-model-secret-leakage.md` §4.12). No scenario change.
|
|
9
|
+
- **Version moved ahead of publication.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.39.5` because `2.39.4` is tagged. Not tagged, not published.
|
|
10
|
+
|
|
3
11
|
## [2.39.4] — 2026-09-25 — RFC 0199 can be witnessed behind a public front, and the pinned-port claim is race-free
|
|
4
12
|
|
|
5
13
|
- **`0199.credential-interrupt` can be witnessed on a host with a public front: `OPENWOP_HOST_PUBLIC_URL`.** RFC 0199 §C.2 requires `connectUrl` to be an https URL "on the host's own origin". The leg compared it to `--base-url`, but a certification cut drives the host over loopback, which can never be the https origin a user agent is sent to. So the v2 reference host, which implements all of §C, could never advertise `oauth.credentialInterrupt` in a cut, and RFC 0199's four credential rows never ran. New `src/lib/host-public-origin.ts` reads an operator-declared front for the host, validated like every other suite front (https, publicly resolvable). It counts as the host's own origin only once the discovery document fetched through it equals the one over `--base-url`; otherwise the leg records `blocked`, so a front pointing at another host can never let a `connectUrl` on that host's origin pass. Unset, behaviour is unchanged (`--base-url`'s origin, and no fetch). Self-test: 5 cases, including a front that serves a different host.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
|
|
12
12
|
# default resolver refuses. npm 10.9 fails outright with
|
|
13
13
|
# "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
|
|
14
|
-
npm install --legacy-peer-deps @openwop/openwop-conformance@2.39.
|
|
14
|
+
npm install --legacy-peer-deps @openwop/openwop-conformance@2.39.5 @openwop/spec-artifacts@2.39.5
|
|
15
15
|
# or run without install:
|
|
16
16
|
npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
|
|
17
17
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.39.
|
|
3
|
+
"version": "2.39.5",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.39.
|
|
59
|
+
"@openwop/spec-artifacts": "2.39.5"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
5
|
"files": 633,
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
6
|
+
"tests": 2425,
|
|
7
|
+
"withStableId": 2425,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2305
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -29279,7 +29279,7 @@
|
|
|
29279
29279
|
{
|
|
29280
29280
|
"id": "openwop.it.v2-idempotency-in-flight.concurrent-same-key-creates-yield-exactly-one-run",
|
|
29281
29281
|
"file": "v2-idempotency-in-flight.test.ts",
|
|
29282
|
-
"line":
|
|
29282
|
+
"line": 105,
|
|
29283
29283
|
"title": "concurrent same-key creates yield exactly one run",
|
|
29284
29284
|
"explicitId": "openwop.requirement.0213.in-flight-one-winner",
|
|
29285
29285
|
"citations": [
|
|
@@ -29298,7 +29298,7 @@
|
|
|
29298
29298
|
{
|
|
29299
29299
|
"id": "openwop.it.v2-idempotency-in-flight.each-loser-is-a-marked-replay-of-a-final-outcome-or-409-idempotency-in-flight-wi",
|
|
29300
29300
|
"file": "v2-idempotency-in-flight.test.ts",
|
|
29301
|
-
"line":
|
|
29301
|
+
"line": 113,
|
|
29302
29302
|
"title": "each loser is a marked replay of a final outcome, or 409 idempotency_in_flight with no retry timing in details",
|
|
29303
29303
|
"explicitId": "openwop.requirement.0213.in-flight-loser-outcome",
|
|
29304
29304
|
"citations": [
|
|
@@ -29324,6 +29324,38 @@
|
|
|
29324
29324
|
}
|
|
29325
29325
|
]
|
|
29326
29326
|
},
|
|
29327
|
+
{
|
|
29328
|
+
"id": "openwop.it.v2-idempotency-in-flight.while-the-seam-holds-the-claim-a-same-key-create-is-refused-409-idempotency-in-f",
|
|
29329
|
+
"file": "v2-idempotency-in-flight.test.ts",
|
|
29330
|
+
"line": 138,
|
|
29331
|
+
"title": "while the seam holds the claim, a same-key create is refused 409 idempotency_in_flight by the host and the held create still wins",
|
|
29332
|
+
"explicitId": "openwop.requirement.0213.in-flight-refused-under-hold",
|
|
29333
|
+
"citations": [
|
|
29334
|
+
{
|
|
29335
|
+
"section": "spec/v2/core/idempotency.md Concurrency",
|
|
29336
|
+
"requirement": null,
|
|
29337
|
+
"interpolated": true
|
|
29338
|
+
},
|
|
29339
|
+
{
|
|
29340
|
+
"section": "spec/v2/core/errors.md §Retry timing",
|
|
29341
|
+
"requirement": "retry timing MUST NOT travel in details"
|
|
29342
|
+
},
|
|
29343
|
+
{
|
|
29344
|
+
"section": "spec/v2/core/idempotency.md Concurrency",
|
|
29345
|
+
"requirement": null,
|
|
29346
|
+
"interpolated": true
|
|
29347
|
+
},
|
|
29348
|
+
{
|
|
29349
|
+
"section": "spec/v2/core/idempotency.md Concurrency",
|
|
29350
|
+
"requirement": null,
|
|
29351
|
+
"interpolated": true
|
|
29352
|
+
},
|
|
29353
|
+
{
|
|
29354
|
+
"section": "spec/v2/core/idempotency.md Concurrency",
|
|
29355
|
+
"requirement": "after the held create completes, a same-key create is a marked replay of the one run"
|
|
29356
|
+
}
|
|
29357
|
+
]
|
|
29358
|
+
},
|
|
29327
29359
|
{
|
|
29328
29360
|
"id": "openwop.it.v2-idempotency-key-grammar.a-22-character-key-inside-the-grammar-is-accepted",
|
|
29329
29361
|
"file": "v2-idempotency-key-grammar.test.ts",
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
|
|
3
3
|
"package": "@openwop/spec-artifacts",
|
|
4
|
-
"version": "2.39.
|
|
5
|
-
"corpusTag": "v2.39.
|
|
4
|
+
"version": "2.39.5",
|
|
5
|
+
"corpusTag": "v2.39.5",
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
8
8
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "6366b5514be71098e9ea41fc81ff52bc39db1a547b353c0b344cc2a2bf5370aa",
|
|
11
11
|
"api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
|
|
12
|
-
"api/seams-v2.yaml": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "ed925b14e1e3e6f82ec0d0c844c035ae1a3b0dbcf10b3e53b96106327298f5cb",
|
|
13
|
+
"api/v2/asyncapi.yaml": "a3ba4c688bd4ee33400ef23718eac3d03bc333df2b3d74644adc6f0b2b3a5b2e",
|
|
14
|
+
"api/v2/openapi.yaml": "e4dd5dafff70be8c64de8344c3ae5dd5525515a069fef6f421a226813a305344",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -204,13 +204,13 @@
|
|
|
204
204
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
205
205
|
"spec/v1/alias-detectors.json": "2401fcb1c18cdd688c018b3d716ae6bca85c5e356220ee2b793bd9d81872412d",
|
|
206
206
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
207
|
-
"spec/v1/core-standard-manifest.json": "
|
|
207
|
+
"spec/v1/core-standard-manifest.json": "a2f4f866565b5da642b36384a235bf57a293366b7f9faeb0e7f62c3132337932",
|
|
208
208
|
"spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
|
|
209
209
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
210
210
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
211
211
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
212
212
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
213
|
-
"spec/v1/gaps.json": "
|
|
213
|
+
"spec/v1/gaps.json": "9424f94441ba5c4d1a8027e1f59be832f46af1a67ff0ca140adca9bb501ee993",
|
|
214
214
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
215
215
|
"spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
|
|
216
216
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -292,9 +292,9 @@
|
|
|
292
292
|
"spec/v2/path-manifest.json": "ae9b56d03a701061fd48ad0b73cb0373493f6a4547da92763b422295b5e19535",
|
|
293
293
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
294
294
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
295
|
-
"spec/v2/release.json": "
|
|
295
|
+
"spec/v2/release.json": "c3d35fbfae242c3fa3a504edb08fbdbc7ee9dd003250ea7225a0f6641dc614b9",
|
|
296
296
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
297
|
-
"spec/v2/surface-baseline.json": "
|
|
297
|
+
"spec/v2/surface-baseline.json": "4273f2b75cfde4c620c995b5a4c334cfc15d936dfa7f5b6fa0aa4bf257e8b8b5"
|
|
298
298
|
},
|
|
299
|
-
"corpusCommit": "
|
|
299
|
+
"corpusCommit": "718fae2de800b73f48a59237cfb843578b0433f1"
|
|
300
300
|
}
|
|
@@ -35,6 +35,25 @@ const defaultFetch: Fetcher = async (url) => {
|
|
|
35
35
|
return { status: res.status, text: await res.text() };
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Replace every occurrence of either origin with one placeholder, in every
|
|
40
|
+
* string value. A host that derives its advertised URLs (`agentCardUrl`, MCP
|
|
41
|
+
* `serverUrls`, …) from the REQUEST origin embeds `http://127.0.0.1:…` in the
|
|
42
|
+
* document fetched over loopback and the tunnel origin in the one fetched
|
|
43
|
+
* through the front — so byte equality could never hold on exactly the hosts
|
|
44
|
+
* this helper exists for (measured on the v2-reference public cut on suite
|
|
45
|
+
* 2.39.4, openwop-1f: the only blocked row). A host with a public base may also
|
|
46
|
+
* embed EITHER origin in EITHER response, so both origins are normalised in both
|
|
47
|
+
* documents. Everything else must still match: a front serving another host
|
|
48
|
+
* differs in far more than its origin.
|
|
49
|
+
*/
|
|
50
|
+
function withoutOrigins(v: unknown, origins: readonly string[]): unknown {
|
|
51
|
+
if (typeof v === 'string') return origins.reduce((acc, o) => acc.split(o).join('<origin>'), v);
|
|
52
|
+
if (Array.isArray(v)) return v.map((x) => withoutOrigins(x, origins));
|
|
53
|
+
if (v !== null && typeof v === 'object') return Object.fromEntries(Object.entries(v as Record<string, unknown>).map(([k, x]) => [k, withoutOrigins(x, origins)]));
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
/** Key-order-independent JSON equality — the two fetches may serialise differently. */
|
|
39
58
|
function canonical(v: unknown): string {
|
|
40
59
|
if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`;
|
|
@@ -63,9 +82,10 @@ export async function hostPublicOrigin(baseUrl: string, fetcher: Fetcher = defau
|
|
|
63
82
|
return { ok: false, reason: `discovery answered ${viaFront.status} through ${HOST_FRONT_ENV}=${frontOrigin} and ${viaBase.status} over --base-url — the declared front is not shown to serve the host under test` };
|
|
64
83
|
}
|
|
65
84
|
let same = false;
|
|
66
|
-
|
|
85
|
+
const origins = [frontOrigin, base.origin];
|
|
86
|
+
try { same = canonical(withoutOrigins(JSON.parse(viaFront.text), origins)) === canonical(withoutOrigins(JSON.parse(viaBase.text), origins)); } catch { same = false; }
|
|
67
87
|
if (!same) {
|
|
68
|
-
return { ok: false, reason: `the discovery document through ${HOST_FRONT_ENV}=${frontOrigin} differs from the one over --base-url — the front does not serve the host under test, so an origin claim through it would witness some other host` };
|
|
88
|
+
return { ok: false, reason: `the discovery document through ${HOST_FRONT_ENV}=${frontOrigin} differs from the one over --base-url beyond their origins — the front does not serve the host under test, so an origin claim through it would witness some other host` };
|
|
69
89
|
}
|
|
70
90
|
return { ok: true, origin: frontOrigin, declared: true };
|
|
71
91
|
}
|
|
@@ -39,10 +39,14 @@ import { v2Discovery } from '../lib/v2.js';
|
|
|
39
39
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
40
40
|
import { softSkip } from '../lib/soft-skip.js';
|
|
41
41
|
import { req } from '../lib/requirement-ids.js';
|
|
42
|
+
import { seamsProfileAdvertised, SEAMS_PREFIX } from '../lib/seams.js';
|
|
42
43
|
|
|
43
44
|
const DOC = 'spec/v2/core/idempotency.md Concurrency';
|
|
44
45
|
const ID_ONE = 'openwop.requirement.0213.in-flight-one-winner';
|
|
45
46
|
const ID_LOSER = 'openwop.requirement.0213.in-flight-loser-outcome';
|
|
47
|
+
const ID_HELD = 'openwop.requirement.0213.in-flight-refused-under-hold';
|
|
48
|
+
const HOLD_SEAM = `${SEAMS_PREFIX}/sample/test/idempotency/hold`;
|
|
49
|
+
const HOLD_MS = 4000;
|
|
46
50
|
const FIXTURE = 'conformance-delay';
|
|
47
51
|
const N = 5;
|
|
48
52
|
|
|
@@ -129,4 +133,31 @@ describe('v2 idempotency-in-flight (idempotency.md Concurrency, RFC 0213 §B)',
|
|
|
129
133
|
// on a row shared with the winner clause.)
|
|
130
134
|
if (r.refusals.length === 0) return softSkip('inapplicable', `no loser was refused in flight — all ${N} answers were successes (each loser a marked replay, which §B permits), so the 409 branch did not run on this host`);
|
|
131
135
|
}, 60_000);
|
|
136
|
+
// The deterministic §B witness (host-sample-test-seams.md §26): the seam only
|
|
137
|
+
// ARMS a hold on the next real create; the 409 is the host's own in-flight branch.
|
|
138
|
+
it('while the seam holds the claim, a same-key create is refused 409 idempotency_in_flight by the host and the held create still wins', async () => {
|
|
139
|
+
const doc = await discovery();
|
|
140
|
+
if (!doc) return softSkip('blocked', 'v2 discovery unreachable');
|
|
141
|
+
if (!seamsProfileAdvertised(doc)) return softSkip('inapplicable', 'the held-claim leg is driven through the seams profile (RFC 0213 §B) — seams profile not advertised; the unaided leg above is this host\'s witness');
|
|
142
|
+
const key = `openwopconf-held-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
143
|
+
const armed = await http(() => driver.post(HOLD_SEAM, { key, holdMs: HOLD_MS }));
|
|
144
|
+
if (armed === null) return softSkip('blocked', `${HOLD_SEAM} unreachable (fetch failed)`);
|
|
145
|
+
if (armed.status === 404 || armed.status === 405 || armed.status === 403) return softSkip('blocked', `the host advertises the seams profile but does not serve ${HOLD_SEAM} (answered ${armed.status}) — the held-claim leg cannot run`);
|
|
146
|
+
if (armed.status !== 201) return softSkip('blocked', `${HOLD_SEAM} answered ${armed.status} ${JSON.stringify(armed.json)} — the seam contract (api/seams-v2.yaml armIdempotencyHold) is 201 { key, holdMs }`);
|
|
147
|
+
const body = { workflowId: 'conformance-noop' };
|
|
148
|
+
const winner = http(() => driver.post('/runs', body, { headers: { 'Idempotency-Key': key } }));
|
|
149
|
+
await new Promise((ok) => setTimeout(ok, 750));
|
|
150
|
+
const loser = await http(() => driver.post('/runs', body, { headers: { 'Idempotency-Key': key } }));
|
|
151
|
+
const won = await winner;
|
|
152
|
+
if (loser === null || won === null) return softSkip('blocked', 'POST /runs unreachable (fetch failed)');
|
|
153
|
+
expect({ status: loser.status, code: readErrorCode(loser.json) }, req(ID_HELD, DOC, `a same-key create while the claim is in flight MUST be refused 409 idempotency_in_flight (got ${loser.status} ${String(readErrorCode(loser.json))})`)).toEqual({ status: 409, code: 'idempotency_in_flight' });
|
|
154
|
+
const details = (loser.json as { details?: Record<string, unknown> } | null)?.details ?? {};
|
|
155
|
+
expect(Object.keys(details).filter((k) => /^retryAfter/i.test(k)), req(ID_HELD, 'spec/v2/core/errors.md §Retry timing', 'retry timing MUST NOT travel in details')).toEqual([]);
|
|
156
|
+
const ra = loser.headers.get('retry-after');
|
|
157
|
+
if (ra !== null) expect(parsesRetryAfter(ra), req(ID_HELD, DOC, `a Retry-After that is present MUST parse (got ${ra})`)).toBe(true);
|
|
158
|
+
expect(won.status >= 200 && won.status < 300, req(ID_HELD, DOC, `the held create MUST complete as the one winner (got ${won.status})`)).toBe(true);
|
|
159
|
+
const replay = await http(() => driver.post('/runs', body, { headers: { 'Idempotency-Key': key } }));
|
|
160
|
+
if (replay === null) return softSkip('blocked', 'POST /runs unreachable after the hold (fetch failed)');
|
|
161
|
+
expect({ replay: replay.headers.get('openwop-idempotent-replay'), runId: (replay.json as { runId?: unknown } | null)?.runId }, req(ID_HELD, DOC, 'after the held create completes, a same-key create is a marked replay of the one run')).toEqual({ replay: 'true', runId: (won.json as { runId?: unknown } | null)?.runId });
|
|
162
|
+
}, 30_000);
|
|
132
163
|
});
|
|
@@ -47,7 +47,7 @@ describe('v2 revocation-honored (RFC 0170 §B.3 — seam-gated)', () => {
|
|
|
47
47
|
for (const lane of nextRequest) {
|
|
48
48
|
const minted = await http(() => driver.post(MINT, { lane }));
|
|
49
49
|
if (minted === null) return softSkip('blocked', `${MINT} unreachable (fetch failed)`);
|
|
50
|
-
if (minted.status === 404 || minted.status === 403) return seamAbsent(`${MINT} not mounted (${minted.status}) — the
|
|
50
|
+
if (minted.status === 404 || minted.status === 403) return seamAbsent(`${MINT} not mounted (${minted.status}) — the host advertises the seams profile but does not serve the RFC 0170 §B.3 credential mint/revoke seams (api/seams-v2.yaml mintLaneCredential / revokeLaneCredential; host-sample-test-seams.md)`);
|
|
51
51
|
const credential = (minted.json as { credential?: unknown } | undefined)?.credential;
|
|
52
52
|
expect(typeof credential, req('openwop.requirement.0170.revocation-honored', DOC, `the mint seam MUST answer { credential } for lane ${lane}`)).toBe('string');
|
|
53
53
|
|