@openwop/openwop-conformance 2.1.2 → 2.1.4
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 +17 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +20 -5
- package/schemas/CORPUS-STAMP.json +11 -11
- package/src/scenarios/replay-divergence-at-refusal.test.ts +51 -3
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +61 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.4] — 2026-09-12 — the replay reds now say why
|
|
4
|
+
|
|
5
|
+
`replay-observable-sequence-determinism` and `replay-divergence-at-refusal`
|
|
6
|
+
asserted `status === 'completed'` bare, discarding the run's `error` and
|
|
7
|
+
`currentNodeId`, so a red reported `expected 'failed' to be 'completed'` and
|
|
8
|
+
nothing else. Both now attach the terminal detail, and both honour
|
|
9
|
+
`OPENWOP_POLL_TIMEOUT_SCALE` — they were the only two scenarios that rolled
|
|
10
|
+
their own poll loop and never read it. No assertion changed meaning.
|
|
11
|
+
|
|
12
|
+
## [2.1.3] — 2026-09-11 — `since` is the host's minor, said in the schema
|
|
13
|
+
|
|
14
|
+
No scenario changed. The contract peer's generated
|
|
15
|
+
`schemas/v2/capabilities.schema.json` now annotates the capability record's
|
|
16
|
+
`since`: it names the minor of **this host's** contract at which it began
|
|
17
|
+
serving the family, not the corpus minor that introduced it. Two hosts read
|
|
18
|
+
it the other way on the same day and `core/capabilities.md` §2 never said.
|
|
19
|
+
|
|
3
20
|
## [2.1.2] — 2026-09-11 — `v2-version-header-honored` failed conformant single-major hosts
|
|
4
21
|
|
|
5
22
|
Leg 1 compared a header-less fetch with an `OpenWOP-Version: 2.0` fetch on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
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.1.
|
|
59
|
+
"@openwop/spec-artifacts": "2.1.4"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -21552,7 +21552,7 @@
|
|
|
21552
21552
|
{
|
|
21553
21553
|
"id": "openwop.it.replay-divergence-at-refusal.replaydeterminism-when-present-conforms-to-rfc-0041-d",
|
|
21554
21554
|
"file": "replay-divergence-at-refusal.test.ts",
|
|
21555
|
-
"line":
|
|
21555
|
+
"line": 111,
|
|
21556
21556
|
"title": "replayDeterminism (when present) conforms to RFC 0041 §D",
|
|
21557
21557
|
"explicitId": "openwop.it.replay-divergence-at-refusal.replaydeterminism-when-present-conforms-to-rfc-0041-d",
|
|
21558
21558
|
"citations": [
|
|
@@ -21573,10 +21573,15 @@
|
|
|
21573
21573
|
{
|
|
21574
21574
|
"id": "openwop.it.replay-divergence-at-refusal.phase-4-host-must-emit-replay-divergedatrefusal-fail-with-replay-diverged-at-ref",
|
|
21575
21575
|
"file": "replay-divergence-at-refusal.test.ts",
|
|
21576
|
-
"line":
|
|
21576
|
+
"line": 227,
|
|
21577
21577
|
"title": "Phase 4 host MUST emit replay.divergedAtRefusal + fail with replay_diverged_at_refusal when original=valid + replay=refusal",
|
|
21578
21578
|
"explicitId": "openwop.it.replay-divergence-at-refusal.phase-4-host-must-emit-replay-divergedatrefusal-fail-with-replay-diverged-at-ref",
|
|
21579
21579
|
"citations": [
|
|
21580
|
+
{
|
|
21581
|
+
"section": "RFCS/0041-multi-agent-replay-under-nondeterminism.md §D",
|
|
21582
|
+
"requirement": null,
|
|
21583
|
+
"interpolated": true
|
|
21584
|
+
},
|
|
21580
21585
|
{
|
|
21581
21586
|
"section": "RFCS/0041-multi-agent-replay-under-nondeterminism.md §B + spec/v1/rest-endpoints.md §\"Common error codes\"",
|
|
21582
21587
|
"requirement": "replay MUST terminate `failed` when refusal-divergence is detected (silent substitution is non-conformant)"
|
|
@@ -21602,7 +21607,7 @@
|
|
|
21602
21607
|
{
|
|
21603
21608
|
"id": "openwop.it.replay-divergence-at-refusal.phase-4-host-must-emit-replay-divergedatrefusal-fail-with-replay-diverged-at-ref~2",
|
|
21604
21609
|
"file": "replay-divergence-at-refusal.test.ts",
|
|
21605
|
-
"line":
|
|
21610
|
+
"line": 320,
|
|
21606
21611
|
"title": "Phase 4 host MUST emit replay.divergedAtRefusal + fail with replay_diverged_at_refusal when original=refusal + replay=valid (symmetric case)",
|
|
21607
21612
|
"explicitId": "openwop.it.replay-divergence-at-refusal.phase-4-host-must-emit-replay-divergedatrefusal-fail-with-replay-diverged-at-ref~2",
|
|
21608
21613
|
"citations": [
|
|
@@ -22001,10 +22006,15 @@
|
|
|
22001
22006
|
{
|
|
22002
22007
|
"id": "openwop.it.replay-observable-sequence-determinism.original-and-replay-event-log-prefixes-must-be-byte-equivalent-modulo-per-event",
|
|
22003
22008
|
"file": "replay-observable-sequence-determinism.test.ts",
|
|
22004
|
-
"line":
|
|
22009
|
+
"line": 210,
|
|
22005
22010
|
"title": "original and replay event-log prefixes MUST be byte-equivalent (modulo per-event clock + ULID entropy)",
|
|
22006
22011
|
"explicitId": "openwop.it.replay-observable-sequence-determinism.original-and-replay-event-log-prefixes-must-be-byte-equivalent-modulo-per-event",
|
|
22007
22012
|
"citations": [
|
|
22013
|
+
{
|
|
22014
|
+
"section": "RFCS/0041-multi-agent-replay-under-nondeterminism.md §C",
|
|
22015
|
+
"requirement": null,
|
|
22016
|
+
"interpolated": true
|
|
22017
|
+
},
|
|
22008
22018
|
{
|
|
22009
22019
|
"section": "RFCS/0041-multi-agent-replay-under-nondeterminism.md §C",
|
|
22010
22020
|
"requirement": "a mode:replay fork MUST reproduce the original observable event-log sequence byte-for-byte modulo volatile per-event fields (eventId/ULID entropy, per-region observedAt clock)"
|
|
@@ -22014,7 +22024,7 @@
|
|
|
22014
22024
|
{
|
|
22015
22025
|
"id": "openwop.it.replay-observable-sequence-determinism.replay-of-a-nondeterministic-tool-node-reproduces-the-original-observable-result",
|
|
22016
22026
|
"file": "replay-observable-sequence-determinism.test.ts",
|
|
22017
|
-
"line":
|
|
22027
|
+
"line": 251,
|
|
22018
22028
|
"title": "replay of a nondeterministic tool node reproduces the ORIGINAL observable result, NOT a fresh call",
|
|
22019
22029
|
"explicitId": "openwop.it.replay-observable-sequence-determinism.replay-of-a-nondeterministic-tool-node-reproduces-the-original-observable-result",
|
|
22020
22030
|
"citations": [
|
|
@@ -22023,6 +22033,11 @@
|
|
|
22023
22033
|
"requirement": null,
|
|
22024
22034
|
"interpolated": true
|
|
22025
22035
|
},
|
|
22036
|
+
{
|
|
22037
|
+
"section": "RFCS/0041-multi-agent-replay-under-nondeterminism.md §C",
|
|
22038
|
+
"requirement": null,
|
|
22039
|
+
"interpolated": true
|
|
22040
|
+
},
|
|
22026
22041
|
{
|
|
22027
22042
|
"section": "RFCS/0041-multi-agent-replay-under-nondeterminism.md §C",
|
|
22028
22043
|
"requirement": "the nondeterministic tool node MUST replay its ORIGINAL observable result (cached event-log entry) rather than re-executing — bit-equivalent re-execution would require unbounded caching, rejected per RFC 0041 §\"Alternatives considered\" #2"
|
|
@@ -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.1.
|
|
5
|
-
"corpusTag": "v2.1.
|
|
4
|
+
"version": "2.1.4",
|
|
5
|
+
"corpusTag": "v2.1.4",
|
|
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": "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": "277fb0c788ff5a085e3bb145e55db05ce002202f6c89a88429cf06187d07f610",
|
|
13
|
+
"api/v2/asyncapi.yaml": "7b8dcd7ca9b6be23b6e81ca19ceb6620c97b7df2840d68c547773895d17220aa",
|
|
14
|
+
"api/v2/openapi.yaml": "6afad97fbcfdfeef0776651f6264a27f5598ac8301a20842a9c739c8ccef3ad4",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"schemas/v2/artifact-type-pack-manifest.schema.json": "85630d1b760d0b41cf5a6fad2acfada39abaf7f0befd95cc4334e1e883e9615d",
|
|
116
116
|
"schemas/v2/audit-verify-result.schema.json": "a748dfadb5155adfde8a91045e76b0db6695005fe559035d71670fdd55edb3ba",
|
|
117
117
|
"schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
|
|
118
|
-
"schemas/v2/capabilities.schema.json": "
|
|
118
|
+
"schemas/v2/capabilities.schema.json": "fa9b80a9107fc322ecadbac808e7fbe5e16ccf0af1a6bf673181faa8379dde33",
|
|
119
119
|
"schemas/v2/certification-bundle.schema.json": "d2cbc5cadddff13630a528dc9673162f582c035c6a6663ca02c1ac53e8fec40c",
|
|
120
120
|
"schemas/v2/channel-presence-payload.schema.json": "9475d46f358d21c19126e78204987a5633979917a2293653354ef5c5e044d54e",
|
|
121
121
|
"schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
201
201
|
"spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
|
|
202
202
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
203
|
-
"spec/v1/core-standard-manifest.json": "
|
|
203
|
+
"spec/v1/core-standard-manifest.json": "9b9a20963e9e364b0bb784a2fb38c376208393214e7ece962c9961fce03b1623",
|
|
204
204
|
"spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
|
|
205
205
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
206
206
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -212,14 +212,14 @@
|
|
|
212
212
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
213
213
|
"spec/v1/operation-path-manifest.json": "5f5f4e3842669371730ebbd1aace3fb794192615f0018dc144f484ba5db82ac3",
|
|
214
214
|
"spec/v1/spec-gaps.json": "6cc9962c6b969f632e07a78b52a4f61447ff579e2990cbae989866f584a86042",
|
|
215
|
-
"spec/v2/README.md": "
|
|
215
|
+
"spec/v2/README.md": "56a157cb6e35e7313d05514b438b4e148d679f6e084b7022431d8f2e81a519b0",
|
|
216
216
|
"spec/v2/core/capabilities.md": "c5107a4964e487c20bbb5254cfe1f706e0d9ba7e34da9070bc43b50b385e7948",
|
|
217
217
|
"spec/v2/core/conformance.md": "622084e3c74c4549087f4d861b1bb98cec201a0c5aec23788d27f53a355fde3b",
|
|
218
218
|
"spec/v2/core/connection-packs.md": "1d759681e2962f5103228c52c45f24607ed4bb9ae73e30605f690c1d5a59e9ee",
|
|
219
219
|
"spec/v2/core/errors.md": "74fdb5862f432a831cf0c2eadbe0c8cca6b2660ec578180ae33db54773a1df28",
|
|
220
220
|
"spec/v2/core/events.md": "defe0969d6ec7c980f415cc0d78b9d065782ac7e5638b12a44aa4895a35d6126",
|
|
221
221
|
"spec/v2/core/form-content-packs.md": "6ffaae85d4550be48dd53fb6e2959af8aceb35bd87c5343b722885084f197713",
|
|
222
|
-
"spec/v2/core/headers.md": "
|
|
222
|
+
"spec/v2/core/headers.md": "35f9b7e912080bb4ff28d06dc1679a9b2857fcff6ce810d6df266bc66d2b13f2",
|
|
223
223
|
"spec/v2/core/idempotency.md": "b86c352d964aebad532727f314e26ed2177d49de382076fa64d3162e2f56118d",
|
|
224
224
|
"spec/v2/core/identity.md": "dd5299f9cdf54640870e1fffae11edb820cf6396ea298c4ebd14c649c4bd4a87",
|
|
225
225
|
"spec/v2/core/interop.md": "d4c404e469e1d9d9be88bc2c939d777e7d7c1d0419b1251c18770f039838a401",
|
|
@@ -270,8 +270,8 @@
|
|
|
270
270
|
"spec/v2/path-manifest.json": "a6f4e652e866889e8bde815874062f62929aee95dc8e58e7152c56adee262163",
|
|
271
271
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
272
272
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
273
|
-
"spec/v2/release.json": "
|
|
273
|
+
"spec/v2/release.json": "31797591070c2c06955915a0348dc9964d162e5fd1b544e073d667cbf78d9756",
|
|
274
274
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
275
275
|
},
|
|
276
|
-
"corpusCommit": "
|
|
276
|
+
"corpusCommit": "5e9633c7297c993efc5a8198cd07dffcd2213e11"
|
|
277
277
|
}
|
|
@@ -75,6 +75,38 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
|
|
|
75
75
|
} catch { return null; }
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Why this says more than `expect(status).toBe('completed')`.
|
|
80
|
+
*
|
|
81
|
+
* A terminal run that is `failed` carries `error` (`schemas/run-snapshot.schema.json`),
|
|
82
|
+
* and the bare equality assertion threw it away: every occurrence of this red
|
|
83
|
+
* reported only `expected 'failed' to be 'completed'`, with no node, no code and
|
|
84
|
+
* no message. A tier-1 host carried exactly that line in its notes across three
|
|
85
|
+
* lanes without ever being able to diagnose it (reported 2026-09-12), because
|
|
86
|
+
* the suite had recorded that something went wrong and nothing about what.
|
|
87
|
+
*
|
|
88
|
+
* That is the suite owing hosts what RFC 0064 §F makes a host owe the wire: a
|
|
89
|
+
* failure MUST be self-describing. This does not make the flake go away — the
|
|
90
|
+
* cause is still unknown, and pretending otherwise would be the third instrument
|
|
91
|
+
* this week that answers a narrower question than it appears to. It makes the
|
|
92
|
+
* next occurrence carry its own evidence.
|
|
93
|
+
*/
|
|
94
|
+
function terminalDetail(snap: RunSnapshot): string {
|
|
95
|
+
if (snap.status === 'completed') return '';
|
|
96
|
+
const err = snap.error as Record<string, unknown> | undefined;
|
|
97
|
+
const parts: string[] = [`status=${String(snap.status)}`];
|
|
98
|
+
if (snap.currentNodeId) parts.push(`currentNodeId=${String(snap.currentNodeId)}`);
|
|
99
|
+
if (err && typeof err === 'object') {
|
|
100
|
+
for (const k of ['code', 'message', 'nodeId', 'retriable']) {
|
|
101
|
+
if (err[k] !== undefined) parts.push(`error.${k}=${JSON.stringify(err[k])}`);
|
|
102
|
+
}
|
|
103
|
+
if (!('code' in err) && !('message' in err)) parts.push(`error=${JSON.stringify(err).slice(0, 300)}`);
|
|
104
|
+
} else {
|
|
105
|
+
parts.push('error=(absent — the host reported a non-completed terminal state with no error object)');
|
|
106
|
+
}
|
|
107
|
+
return ` — ${parts.join(' ')}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
78
110
|
describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: advertisement shape (RFC 0041 §D)', () => {
|
|
79
111
|
it('replayDeterminism (when present) conforms to RFC 0041 §D', async (ctx) => {
|
|
80
112
|
const d = await readDiscovery();
|
|
@@ -127,6 +159,7 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: advertisement shape (R
|
|
|
127
159
|
interface RunSnapshot {
|
|
128
160
|
status?: string;
|
|
129
161
|
error?: { code?: string; message?: string };
|
|
162
|
+
currentNodeId?: string;
|
|
130
163
|
}
|
|
131
164
|
interface RunEventDoc {
|
|
132
165
|
type: string;
|
|
@@ -135,8 +168,16 @@ interface RunEventDoc {
|
|
|
135
168
|
payload?: Record<string, unknown>;
|
|
136
169
|
}
|
|
137
170
|
|
|
171
|
+
// The 5s budget below is scaled by OPENWOP_POLL_TIMEOUT_SCALE like every
|
|
172
|
+
// other bound in the suite. `lib/polling.ts` exists so a timeout can say
|
|
173
|
+
// whether it measured the host or the harness — *"observe no change in
|
|
174
|
+
// those scenarios, and record a failure that measured the environment
|
|
175
|
+
// rather than the host"* — and these two replay files were the only ones
|
|
176
|
+
// that rolled their own loop and never read it.
|
|
138
177
|
async function pollUntilTerminal(runId: string): Promise<RunSnapshot> {
|
|
139
|
-
|
|
178
|
+
const scale = Number(process.env['OPENWOP_POLL_TIMEOUT_SCALE'] ?? '1');
|
|
179
|
+
const rounds = Math.max(1, Math.round(50 * (Number.isFinite(scale) && scale > 0 ? scale : 1)));
|
|
180
|
+
for (let i = 0; i < rounds; i++) {
|
|
140
181
|
const r = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
|
|
141
182
|
const snap = r.json as RunSnapshot;
|
|
142
183
|
if (snap.status === 'completed' || snap.status === 'failed' || snap.status === 'cancelled') {
|
|
@@ -144,7 +185,7 @@ async function pollUntilTerminal(runId: string): Promise<RunSnapshot> {
|
|
|
144
185
|
}
|
|
145
186
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
146
187
|
}
|
|
147
|
-
throw new Error(`run ${runId} did not reach terminal within
|
|
188
|
+
throw new Error(`run ${runId} did not reach a terminal state within ${rounds * 100}ms (OPENWOP_POLL_TIMEOUT_SCALE=${process.env['OPENWOP_POLL_TIMEOUT_SCALE'] ?? '1'})`);
|
|
148
189
|
}
|
|
149
190
|
|
|
150
191
|
async function readEvents(runId: string): Promise<RunEventDoc[]> {
|
|
@@ -212,7 +253,14 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: behavioral (RFC 0041
|
|
|
212
253
|
expect(createRes.status).toBe(201);
|
|
213
254
|
const sourceRunId = (createRes.json as { runId: string }).runId;
|
|
214
255
|
const sourceTerminal = await pollUntilTerminal(sourceRunId);
|
|
215
|
-
expect(
|
|
256
|
+
expect(
|
|
257
|
+
sourceTerminal.status,
|
|
258
|
+
req(
|
|
259
|
+
'openwop.it.replay-divergence-at-refusal.phase-4-host-must-emit-replay-divergedatrefusal-fail-with-replay-diverged-at-ref',
|
|
260
|
+
'RFCS/0041-multi-agent-replay-under-nondeterminism.md §D',
|
|
261
|
+
`the source run MUST reach \`completed\` for the replay comparison to mean anything${terminalDetail(sourceTerminal)}`,
|
|
262
|
+
),
|
|
263
|
+
).toBe('completed');
|
|
216
264
|
|
|
217
265
|
// Stage refusal for the replay's mock-AI dispatch.
|
|
218
266
|
await programMock(NODE_ID, [
|
|
@@ -62,6 +62,8 @@ interface DiscoveryDoc {
|
|
|
62
62
|
|
|
63
63
|
interface RunSnapshot {
|
|
64
64
|
status?: string;
|
|
65
|
+
error?: unknown;
|
|
66
|
+
currentNodeId?: string;
|
|
65
67
|
}
|
|
66
68
|
interface RunEventDoc {
|
|
67
69
|
type: string;
|
|
@@ -93,8 +95,16 @@ async function gateOnPhase4(ctx: { skip: () => void }): Promise<boolean> {
|
|
|
93
95
|
return true;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
// The 5s budget below is scaled by OPENWOP_POLL_TIMEOUT_SCALE like every
|
|
99
|
+
// other bound in the suite. `lib/polling.ts` exists so a timeout can say
|
|
100
|
+
// whether it measured the host or the harness — *"observe no change in
|
|
101
|
+
// those scenarios, and record a failure that measured the environment
|
|
102
|
+
// rather than the host"* — and these two replay files were the only ones
|
|
103
|
+
// that rolled their own loop and never read it.
|
|
96
104
|
async function pollUntilTerminal(runId: string): Promise<RunSnapshot> {
|
|
97
|
-
|
|
105
|
+
const scale = Number(process.env['OPENWOP_POLL_TIMEOUT_SCALE'] ?? '1');
|
|
106
|
+
const rounds = Math.max(1, Math.round(50 * (Number.isFinite(scale) && scale > 0 ? scale : 1)));
|
|
107
|
+
for (let i = 0; i < rounds; i++) {
|
|
98
108
|
const r = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
|
|
99
109
|
const snap = r.json as RunSnapshot;
|
|
100
110
|
if (snap.status === 'completed' || snap.status === 'failed' || snap.status === 'cancelled') {
|
|
@@ -102,7 +112,7 @@ async function pollUntilTerminal(runId: string): Promise<RunSnapshot> {
|
|
|
102
112
|
}
|
|
103
113
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
104
114
|
}
|
|
105
|
-
throw new Error(`run ${runId} did not reach terminal within
|
|
115
|
+
throw new Error(`run ${runId} did not reach a terminal state within ${rounds * 100}ms (OPENWOP_POLL_TIMEOUT_SCALE=${process.env['OPENWOP_POLL_TIMEOUT_SCALE'] ?? '1'})`);
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
async function readEvents(runId: string): Promise<RunEventDoc[]> {
|
|
@@ -162,6 +172,38 @@ async function startFixtureRun(ctx: { skip: () => void }): Promise<string | null
|
|
|
162
172
|
return (create.json as { runId: string }).runId;
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Why this says more than `expect(status).toBe('completed')`.
|
|
177
|
+
*
|
|
178
|
+
* A terminal run that is `failed` carries `error` (`schemas/run-snapshot.schema.json`),
|
|
179
|
+
* and the bare equality assertion threw it away: every occurrence of this red
|
|
180
|
+
* reported only `expected 'failed' to be 'completed'`, with no node, no code and
|
|
181
|
+
* no message. A tier-1 host carried exactly that line in its notes across three
|
|
182
|
+
* lanes without ever being able to diagnose it (reported 2026-09-12), because
|
|
183
|
+
* the suite had recorded that something went wrong and nothing about what.
|
|
184
|
+
*
|
|
185
|
+
* That is the suite owing hosts what RFC 0064 §F makes a host owe the wire: a
|
|
186
|
+
* failure MUST be self-describing. This does not make the flake go away — the
|
|
187
|
+
* cause is still unknown, and pretending otherwise would be the third instrument
|
|
188
|
+
* this week that answers a narrower question than it appears to. It makes the
|
|
189
|
+
* next occurrence carry its own evidence.
|
|
190
|
+
*/
|
|
191
|
+
function terminalDetail(snap: RunSnapshot): string {
|
|
192
|
+
if (snap.status === 'completed') return '';
|
|
193
|
+
const err = snap.error as Record<string, unknown> | undefined;
|
|
194
|
+
const parts: string[] = [`status=${String(snap.status)}`];
|
|
195
|
+
if (snap.currentNodeId) parts.push(`currentNodeId=${String(snap.currentNodeId)}`);
|
|
196
|
+
if (err && typeof err === 'object') {
|
|
197
|
+
for (const k of ['code', 'message', 'nodeId', 'retriable']) {
|
|
198
|
+
if (err[k] !== undefined) parts.push(`error.${k}=${JSON.stringify(err[k])}`);
|
|
199
|
+
}
|
|
200
|
+
if (!('code' in err) && !('message' in err)) parts.push(`error=${JSON.stringify(err).slice(0, 300)}`);
|
|
201
|
+
} else {
|
|
202
|
+
parts.push('error=(absent — the host reported a non-completed terminal state with no error object)');
|
|
203
|
+
}
|
|
204
|
+
return ` — ${parts.join(' ')}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
165
207
|
describe.skipIf(HTTP_SKIP)(
|
|
166
208
|
'replay-observable-sequence-determinism: prefix byte-equivalence (RFC 0041 §C)',
|
|
167
209
|
() => {
|
|
@@ -171,7 +213,14 @@ describe.skipIf(HTTP_SKIP)(
|
|
|
171
213
|
const sourceRunId = await startFixtureRun(ctx);
|
|
172
214
|
if (sourceRunId === null) return softSkip('blocked', 'precondition not met — `sourceRunId === null` returned early (seam, prior step, or fixture unavailable)');
|
|
173
215
|
const sourceTerminal = await pollUntilTerminal(sourceRunId);
|
|
174
|
-
expect(
|
|
216
|
+
expect(
|
|
217
|
+
sourceTerminal.status,
|
|
218
|
+
req(
|
|
219
|
+
'openwop.it.replay-observable-sequence-determinism.original-and-replay-event-log-prefixes-must-be-byte-equivalent-modulo-per-event',
|
|
220
|
+
'RFCS/0041-multi-agent-replay-under-nondeterminism.md §C',
|
|
221
|
+
`the source run MUST reach \`completed\` for the replay comparison to mean anything${terminalDetail(sourceTerminal)}`,
|
|
222
|
+
),
|
|
223
|
+
).toBe('completed');
|
|
175
224
|
const sourceEvents = await readEvents(sourceRunId);
|
|
176
225
|
|
|
177
226
|
const forkRes = await driver.post(`/v1/runs/${encodeURIComponent(sourceRunId)}:fork`, {
|
|
@@ -204,7 +253,15 @@ describe.skipIf(HTTP_SKIP)(
|
|
|
204
253
|
|
|
205
254
|
const sourceRunId = await startFixtureRun(ctx);
|
|
206
255
|
if (sourceRunId === null) return softSkip('blocked', 'precondition not met — `sourceRunId === null` returned early (seam, prior step, or fixture unavailable)');
|
|
207
|
-
|
|
256
|
+
const t = await pollUntilTerminal(sourceRunId);
|
|
257
|
+
expect(
|
|
258
|
+
t.status,
|
|
259
|
+
req(
|
|
260
|
+
'openwop.it.replay-observable-sequence-determinism.replay-of-a-nondeterministic-tool-node-reproduces-the-original-observable-result',
|
|
261
|
+
'RFCS/0041-multi-agent-replay-under-nondeterminism.md §C',
|
|
262
|
+
`the source run MUST reach \`completed\` for the replay comparison to mean anything${terminalDetail(t)}`,
|
|
263
|
+
),
|
|
264
|
+
).toBe('completed');
|
|
208
265
|
const sourceEvents = await readEvents(sourceRunId);
|
|
209
266
|
|
|
210
267
|
// The terminal event(s) for the nondeterministic node carry its
|