@openwop/openwop-conformance 1.156.0 → 1.159.0
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
|
@@ -103,7 +103,8 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
|
|
|
103
103
|
|
|
104
104
|
## What's Covered
|
|
105
105
|
|
|
106
|
-
The current suite has
|
|
106
|
+
The current suite has 473 scenario files under `src/scenarios/`.
|
|
107
|
+
- 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.
|
|
107
108
|
- 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.
|
|
108
109
|
- 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.**
|
|
109
110
|
- 2026-08-18 (suite `1.136.7 → 1.136.8`, SP-04): NEW `spec-section-citations.test.ts` — server-free; a `<doc>.md §"<Section>"` citation of a checked doc MUST resolve to a heading that exists. `storage-adapters.md §"Claim acquisition"` was cited by four artifacts, including a normative MUST in `production-profile.md` §Durability, and the section did not exist for the life of RFC 0009. Scoped to the docs whose section citations carry the durability contract; a corpus-wide sweep finds ~260 unresolved citations that need triage before they can gate.
|
|
@@ -447,7 +448,7 @@ Server-required (added in 1.7.0):
|
|
|
447
448
|
| ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
448
449
|
| **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. |
|
|
449
450
|
|
|
450
|
-
Current source tree:
|
|
451
|
+
Current source tree: 473 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
|
|
451
452
|
|
|
452
453
|
## Remaining Gaps
|
|
453
454
|
|
package/package.json
CHANGED
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": 473,
|
|
6
|
+
"tests": 1974,
|
|
7
|
+
"withStableId": 1951,
|
|
8
8
|
"interpolatedTitles": 23,
|
|
9
9
|
"explicitIds": 0
|
|
10
10
|
},
|
|
@@ -11445,6 +11445,65 @@
|
|
|
11445
11445
|
}
|
|
11446
11446
|
]
|
|
11447
11447
|
},
|
|
11448
|
+
{
|
|
11449
|
+
"id": "openwop.it.idempotency-concurrent-claim.every-executor-mints-the-same-logicalinvocationid-without-this-one-delivery-prov",
|
|
11450
|
+
"file": "idempotency-concurrent-claim.test.ts",
|
|
11451
|
+
"line": 81,
|
|
11452
|
+
"title": "every executor mints the SAME logicalInvocationId — without this, one delivery proves nothing",
|
|
11453
|
+
"explicitId": null,
|
|
11454
|
+
"citations": [
|
|
11455
|
+
{
|
|
11456
|
+
"section": "host-sample-test-seams.md §25",
|
|
11457
|
+
"requirement": "the seam MUST report mintedIds, one per executor"
|
|
11458
|
+
},
|
|
11459
|
+
{
|
|
11460
|
+
"section": "host-sample-test-seams.md §25",
|
|
11461
|
+
"requirement": "mintedIds length MUST equal the executors that reached the chokepoint"
|
|
11462
|
+
},
|
|
11463
|
+
{
|
|
11464
|
+
"section": "host-sample-test-seams.md §25",
|
|
11465
|
+
"requirement": "fewer than two executors is not a race and cannot witness the rule"
|
|
11466
|
+
},
|
|
11467
|
+
{
|
|
11468
|
+
"section": "idempotency.md §\"Idempotency key composition\" (Across a recovery boundary)",
|
|
11469
|
+
"requirement": "all executors MUST mint one identity — differing ids mean they never collided, so a single delivery is a vacuous pass rather than a working claim"
|
|
11470
|
+
}
|
|
11471
|
+
]
|
|
11472
|
+
},
|
|
11473
|
+
{
|
|
11474
|
+
"id": "openwop.it.idempotency-concurrent-claim.exactly-one-effect-escapes-however-many-executors-attempt-it",
|
|
11475
|
+
"file": "idempotency-concurrent-claim.test.ts",
|
|
11476
|
+
"line": 110,
|
|
11477
|
+
"title": "exactly one effect escapes, however many executors attempt it",
|
|
11478
|
+
"explicitId": null,
|
|
11479
|
+
"citations": [
|
|
11480
|
+
{
|
|
11481
|
+
"section": "host-sample-test-seams.md §25",
|
|
11482
|
+
"requirement": "the seam MUST report how many executors attempted the effect"
|
|
11483
|
+
},
|
|
11484
|
+
{
|
|
11485
|
+
"section": "idempotency.md §\"Concurrent duplicates (Layer 2)\"",
|
|
11486
|
+
"requirement": "the engine MUST ensure at most one concurrent executor performs the external effect — the guarding persist MUST be an atomic claim, not a read-then-write"
|
|
11487
|
+
}
|
|
11488
|
+
]
|
|
11489
|
+
},
|
|
11490
|
+
{
|
|
11491
|
+
"id": "openwop.it.idempotency-concurrent-claim.the-race-is-real-at-higher-concurrency-too-a-claim-that-only-holds-at-2-is-not-a",
|
|
11492
|
+
"file": "idempotency-concurrent-claim.test.ts",
|
|
11493
|
+
"line": 127,
|
|
11494
|
+
"title": "the race is real at higher concurrency too — a claim that only holds at 2 is not a claim",
|
|
11495
|
+
"explicitId": null,
|
|
11496
|
+
"citations": [
|
|
11497
|
+
{
|
|
11498
|
+
"section": "idempotency.md §\"Concurrent duplicates (Layer 2)\"",
|
|
11499
|
+
"requirement": "exactly one effect regardless of how many executors race — at most one wins the compare-and-set, the rest observe the hit"
|
|
11500
|
+
},
|
|
11501
|
+
{
|
|
11502
|
+
"section": "idempotency.md §\"Idempotency key composition\"",
|
|
11503
|
+
"requirement": "one identity across all executors at any concurrency"
|
|
11504
|
+
}
|
|
11505
|
+
]
|
|
11506
|
+
},
|
|
11448
11507
|
{
|
|
11449
11508
|
"id": "openwop.it.idempotency-key-determinism.skips-cleanly-when-packs-is-not-bundled",
|
|
11450
11509
|
"file": "idempotency-key-determinism.test.ts",
|
|
@@ -14608,7 +14667,7 @@
|
|
|
14608
14667
|
{
|
|
14609
14668
|
"id": "openwop.it.owner-subject-echo.subject-is-consistent-with-the-owner-triple-b-2-and-the-actor-chain-is-bounded-b",
|
|
14610
14669
|
"file": "owner-subject-echo.test.ts",
|
|
14611
|
-
"line":
|
|
14670
|
+
"line": 84,
|
|
14612
14671
|
"title": "subject is consistent with the owner triple (§B.2) and the actor chain is bounded (§B.5)",
|
|
14613
14672
|
"explicitId": null,
|
|
14614
14673
|
"citations": [
|
|
@@ -14641,7 +14700,7 @@
|
|
|
14641
14700
|
{
|
|
14642
14701
|
"id": "openwop.it.owner-subject-echo.run-started-echoes-the-snapshot-subject-verbatim-b-2-rfc-0048-c",
|
|
14643
14702
|
"file": "owner-subject-echo.test.ts",
|
|
14644
|
-
"line":
|
|
14703
|
+
"line": 106,
|
|
14645
14704
|
"title": "run.started echoes the snapshot subject verbatim (§B.2, RFC 0048 §C)",
|
|
14646
14705
|
"explicitId": null,
|
|
14647
14706
|
"citations": [
|
|
@@ -14658,7 +14717,7 @@
|
|
|
14658
14717
|
{
|
|
14659
14718
|
"id": "openwop.it.owner-subject-echo.a-fork-copies-owner-tenant-and-owner-subject-verbatim-onto-the-child-b-4",
|
|
14660
14719
|
"file": "owner-subject-echo.test.ts",
|
|
14661
|
-
"line":
|
|
14720
|
+
"line": 123,
|
|
14662
14721
|
"title": "a fork copies owner.tenant and owner.subject verbatim onto the child (§B.4)",
|
|
14663
14722
|
"explicitId": null,
|
|
14664
14723
|
"citations": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md §\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
|
|
3
|
-
"suiteVersion": "1.
|
|
4
|
-
"corpusCommit": "
|
|
3
|
+
"suiteVersion": "1.159.0",
|
|
4
|
+
"corpusCommit": "1be7b47172478567f8690acb23d570d135bf8a40",
|
|
5
5
|
"files": {
|
|
6
6
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
7
7
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" — the
|
|
3
|
+
* atomic-claim witness. Gap **G17**.
|
|
4
|
+
*
|
|
5
|
+
* The rule: the persist that guards a side effect MUST be an **atomic claim** —
|
|
6
|
+
* a compare-and-set / insert-if-absent that at most one executor can win — not a
|
|
7
|
+
* read-then-write. It is unconditional, and the spec says it MUST hold *within a
|
|
8
|
+
* single-instance deployment*, naming the case where an orphan sweeper
|
|
9
|
+
* re-dispatches a run whose owner is stalled but still alive.
|
|
10
|
+
*
|
|
11
|
+
* It had no witness of any kind. `layer2-invocation-claim-atomic` was registered
|
|
12
|
+
* reference-impl tier with an EMPTY `tests` list, because driving the rule needs
|
|
13
|
+
* two executors of one run concurrently reaching one effect chokepoint and the
|
|
14
|
+
* black-box suite cannot cause that. Per RFC 0148 §A an unobservable requirement
|
|
15
|
+
* resolves to `blocked` — and a wire probe asserting it would pass on a host that
|
|
16
|
+
* has the defect, which is the vacuous-witness pattern §A exists to close.
|
|
17
|
+
*
|
|
18
|
+
* `host-sample-test-seams.md` §25 is what makes it drivable. This file drives it.
|
|
19
|
+
*
|
|
20
|
+
* **Two assertions, and the second is the point.** `delivered === 1` alone is not
|
|
21
|
+
* evidence: a host passes it by minting DIFFERENT identities per executor and
|
|
22
|
+
* never colliding — one effect, because nothing raced. So the seam reports the
|
|
23
|
+
* id each executor minted and this scenario asserts they are identical BEFORE
|
|
24
|
+
* asserting the delivery count. That is the observable projection of
|
|
25
|
+
* `idempotency.md` §"Idempotency key composition" → "Across a recovery boundary":
|
|
26
|
+
* the ordinal reproduces iff the resumed unit re-executes the node's logical
|
|
27
|
+
* activities from the start, and a host that resumes mid-node shifts every
|
|
28
|
+
* downstream identity and defeats Layer-2 dedup on exactly the crash it most
|
|
29
|
+
* needs to survive.
|
|
30
|
+
*
|
|
31
|
+
* Reproducing the race is the hard part, and a true statement can stand in the
|
|
32
|
+
* way of it: a tier-1 host carried the note "the shared ordinal counter hands the
|
|
33
|
+
* second emit a DIFFERENT identity, so it is not reproducible single-instance" —
|
|
34
|
+
* true of two SEQUENTIAL emits, and it became a reason nobody tried. Identity
|
|
35
|
+
* minting runs synchronously before the first `await`, so two un-awaited calls
|
|
36
|
+
* both mint the same ordinal. That host then measured 2 effects without the
|
|
37
|
+
* claim and 1 with it.
|
|
38
|
+
*
|
|
39
|
+
* A host that does not mount the seam records `blocked` (unwitnessed), never
|
|
40
|
+
* `inapplicable`: the requirement binds every host, so absence is missing
|
|
41
|
+
* evidence rather than a requirement that does not apply.
|
|
42
|
+
*
|
|
43
|
+
* **Deliberately NOT `behaviorGate`, and therefore not opt-out-able.** A profile
|
|
44
|
+
* gate exists to separate "host claims X and fails" from "host does not claim X",
|
|
45
|
+
* and it lets an operator flip strict-mode failure into pass-by-opt-out. Neither
|
|
46
|
+
* move is available here: this obligation is UNCONDITIONAL, so no host may
|
|
47
|
+
* declare it does not apply, and converting an unwitnessed MUST into a pass is
|
|
48
|
+
* precisely the dishonesty the opt-out list must never be used for. `blocked` is
|
|
49
|
+
* already the correct RFC 0148 §A disposition — it applies, it was not witnessed,
|
|
50
|
+
* and it is NOT certifiable. That keeps the reference-host soak green without
|
|
51
|
+
* anyone claiming the requirement was met.
|
|
52
|
+
*/
|
|
53
|
+
import { describe, it, expect } from 'vitest';
|
|
54
|
+
import { driver } from '../lib/driver.js';
|
|
55
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
56
|
+
|
|
57
|
+
const SEAM = '/v1/host/sample/test/idempotency/concurrent-claim';
|
|
58
|
+
|
|
59
|
+
type ClaimResult = {
|
|
60
|
+
logicalInvocationId?: unknown;
|
|
61
|
+
mintedIds?: unknown;
|
|
62
|
+
attempted?: unknown;
|
|
63
|
+
delivered?: unknown;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Is the §25 seam mounted? 404/403/501 mean not mounted — anything else is a real answer. */
|
|
67
|
+
async function seamPresent(): Promise<ClaimResult | null> {
|
|
68
|
+
const res = await driver.post(SEAM, { executors: 2 });
|
|
69
|
+
if (res.status === 404 || res.status === 403 || res.status === 501) return null;
|
|
70
|
+
expect(
|
|
71
|
+
res.status,
|
|
72
|
+
driver.describe(
|
|
73
|
+
'host-sample-test-seams.md §25',
|
|
74
|
+
'a mounted concurrent-claim seam MUST answer 200; a non-200 that is not 404/403/501 is a broken seam, not an absent one',
|
|
75
|
+
),
|
|
76
|
+
).toBe(200);
|
|
77
|
+
return res.json as ClaimResult;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('idempotency-concurrent-claim: two executors of one run, one effect (RFC 0150 §B, G17)', () => {
|
|
81
|
+
it('every executor mints the SAME logicalInvocationId — without this, one delivery proves nothing', async () => {
|
|
82
|
+
const out = await seamPresent();
|
|
83
|
+
if (out === null) return softSkip('blocked', `${SEAM} not mounted (host-sample-test-seams.md §25)`);
|
|
84
|
+
|
|
85
|
+
const minted = out.mintedIds;
|
|
86
|
+
expect(
|
|
87
|
+
Array.isArray(minted),
|
|
88
|
+
driver.describe('host-sample-test-seams.md §25', 'the seam MUST report mintedIds, one per executor'),
|
|
89
|
+
).toBe(true);
|
|
90
|
+
const ids = minted as unknown[];
|
|
91
|
+
expect(
|
|
92
|
+
ids.length,
|
|
93
|
+
driver.describe('host-sample-test-seams.md §25', 'mintedIds length MUST equal the executors that reached the chokepoint'),
|
|
94
|
+
).toBe(out.attempted);
|
|
95
|
+
expect(
|
|
96
|
+
ids.length >= 2,
|
|
97
|
+
driver.describe('host-sample-test-seams.md §25', 'fewer than two executors is not a race and cannot witness the rule'),
|
|
98
|
+
).toBe(true);
|
|
99
|
+
|
|
100
|
+
const unique = new Set(ids.map((x) => JSON.stringify(x)));
|
|
101
|
+
expect(
|
|
102
|
+
unique.size,
|
|
103
|
+
driver.describe(
|
|
104
|
+
'idempotency.md §"Idempotency key composition" (Across a recovery boundary)',
|
|
105
|
+
'all executors MUST mint one identity — differing ids mean they never collided, so a single delivery is a vacuous pass rather than a working claim',
|
|
106
|
+
),
|
|
107
|
+
).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('exactly one effect escapes, however many executors attempt it', async () => {
|
|
111
|
+
const out = await seamPresent();
|
|
112
|
+
if (out === null) return softSkip('blocked', `${SEAM} not mounted (host-sample-test-seams.md §25)`);
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
typeof out.attempted === 'number' && (out.attempted as number) >= 2,
|
|
116
|
+
driver.describe('host-sample-test-seams.md §25', 'the seam MUST report how many executors attempted the effect'),
|
|
117
|
+
).toBe(true);
|
|
118
|
+
expect(
|
|
119
|
+
out.delivered,
|
|
120
|
+
driver.describe(
|
|
121
|
+
'idempotency.md §"Concurrent duplicates (Layer 2)"',
|
|
122
|
+
'the engine MUST ensure at most one concurrent executor performs the external effect — the guarding persist MUST be an atomic claim, not a read-then-write',
|
|
123
|
+
),
|
|
124
|
+
).toBe(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('the race is real at higher concurrency too — a claim that only holds at 2 is not a claim', async () => {
|
|
128
|
+
const probe = await seamPresent();
|
|
129
|
+
if (probe === null) return softSkip('blocked', `${SEAM} not mounted (host-sample-test-seams.md §25)`);
|
|
130
|
+
|
|
131
|
+
const res = await driver.post(SEAM, { executors: 5 });
|
|
132
|
+
// A host MAY cap `executors`; what it MUST NOT do is deliver more than once.
|
|
133
|
+
// Say so — an unclassified return records `blocked` with no reason, which is
|
|
134
|
+
// the same thing this file exists to stop happening to the requirement.
|
|
135
|
+
if (res.status !== 200) {
|
|
136
|
+
return softSkip('skipped', `${SEAM} declined executors: 5 (status ${res.status}) — a host MAY cap concurrency`);
|
|
137
|
+
}
|
|
138
|
+
const out = res.json as ClaimResult;
|
|
139
|
+
expect(
|
|
140
|
+
out.delivered,
|
|
141
|
+
driver.describe(
|
|
142
|
+
'idempotency.md §"Concurrent duplicates (Layer 2)"',
|
|
143
|
+
'exactly one effect regardless of how many executors race — at most one wins the compare-and-set, the rest observe the hit',
|
|
144
|
+
),
|
|
145
|
+
).toBe(1);
|
|
146
|
+
const ids = Array.isArray(out.mintedIds) ? (out.mintedIds as unknown[]) : [];
|
|
147
|
+
if (ids.length >= 2) {
|
|
148
|
+
expect(
|
|
149
|
+
new Set(ids.map((x) => JSON.stringify(x))).size,
|
|
150
|
+
driver.describe('idempotency.md §"Idempotency key composition"', 'one identity across all executors at any concurrency'),
|
|
151
|
+
).toBe(1);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -18,6 +18,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
18
18
|
import { driver } from '../lib/driver.js';
|
|
19
19
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
20
20
|
import { softSkip } from '../lib/soft-skip.js';
|
|
21
|
+
import { pollUntil } from '../lib/polling.js';
|
|
21
22
|
|
|
22
23
|
const NOOP_WORKFLOW_ID = 'conformance-noop';
|
|
23
24
|
const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
|
|
@@ -62,7 +63,13 @@ async function snapshot(runId: string): Promise<Snapshot> {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
async function events(runId: string): Promise<readonly RawEvent[]> {
|
|
65
|
-
|
|
66
|
+
// Suite 1.159.0 — NO cursor. `lastSequence` names the highest sequence the
|
|
67
|
+
// caller has ALREADY observed (version-negotiation.md §"events/poll"), and
|
|
68
|
+
// run-event.schema.json numbers the first event 0, so `lastSequence=0` asks
|
|
69
|
+
// a conforming host to SKIP `run.started` (MyndHyve does exactly that).
|
|
70
|
+
// Omitting the cursor is the only spec-expressible "nothing observed yet"
|
|
71
|
+
// read; the gap (no cursor value for the empty prefix) is RFC 0165 G7.
|
|
72
|
+
const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`);
|
|
66
73
|
if (res.status !== 200) throw new Error(`events for ${runId}: ${res.status}`);
|
|
67
74
|
return (res.json as { events?: RawEvent[] })?.events ?? [];
|
|
68
75
|
}
|
|
@@ -102,6 +109,12 @@ describe.skipIf(SKIP_NO_NOOP)('owner-subject-echo: consistency, echo, fork (RFC
|
|
|
102
109
|
if (snap.owner?.subject === undefined) {
|
|
103
110
|
return softSkip('inapplicable', 'host does not emit owner.subject on new runs (RFC 0165 §B — optional in v1.x)');
|
|
104
111
|
}
|
|
112
|
+
// Suite 1.157.0 — `run.started` is appended when the host STARTS the run,
|
|
113
|
+
// which a conforming host may do asynchronously after the 201 (openwop-app
|
|
114
|
+
// dispatches on the next tick). Reading the log straight after the POST
|
|
115
|
+
// raced that append and failed a conforming host; wait for the run to
|
|
116
|
+
// leave `pending` first, then the event is a fact, not a timing.
|
|
117
|
+
await pollUntil(runId, (s) => s.status !== 'pending', { label: 'run left pending (run.started appended)' });
|
|
105
118
|
const started = (await events(runId)).find((e) => e.type === 'run.started');
|
|
106
119
|
expect(started, driver.describe('RFC 0048 §C', 'run.started MUST be present in the event log')).toBeDefined();
|
|
107
120
|
expect(started?.payload?.owner?.subject, driver.describe('RFC 0165 §B.2', 'run.started owner.subject MUST equal RunSnapshot.owner.subject')).toEqual(snap.owner.subject);
|