@openwop/openwop-conformance 2.0.0-rc.59 → 2.0.0-rc.60

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
@@ -117,7 +117,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
117
117
 
118
118
  ## What's Covered
119
119
 
120
- The current suite has 515 scenario files under `src/scenarios/`.
120
+ The current suite has 516 scenario files under `src/scenarios/`.
121
121
  - 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.
122
122
  - 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.
123
123
  - 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.**
@@ -462,7 +462,7 @@ Server-required (added in 1.7.0):
462
462
  | ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
463
463
  | **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. |
464
464
 
465
- Current source tree: 515 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
465
+ Current source tree: 516 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
466
466
 
467
467
  ## Remaining Gaps
468
468
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.0.0-rc.59",
4
- "stampSha256": "4c5c8d7ef06d876ffd86f942a929aad2c6efda0d896d5fc64fb755a3660d09d5"
3
+ "version": "2.0.0-rc.60",
4
+ "stampSha256": "a8d45fba2fd662735347d91b9dda9e16486b50896a866a54b679a7a0f7129cae"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.0.0-rc.59",
3
+ "version": "2.0.0-rc.60",
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"
59
+ "@openwop/spec-artifacts": "2.0.0-rc.60"
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": 564,
6
- "tests": 2149,
7
- "withStableId": 2148,
5
+ "files": 565,
6
+ "tests": 2150,
7
+ "withStableId": 2149,
8
8
  "interpolatedTitles": 1,
9
9
  "explicitIds": 2058
10
10
  },
@@ -28355,6 +28355,30 @@
28355
28355
  }
28356
28356
  ]
28357
28357
  },
28358
+ {
28359
+ "id": "openwop.it.v2-run-fork-prefix.a-replay-fork-inherits-exactly-0-fromseq-the-event-at-fromseq-is-re-executed-nev",
28360
+ "file": "v2-run-fork-prefix.test.ts",
28361
+ "line": 86,
28362
+ "title": "a replay fork inherits exactly [0, fromSeq): the event at fromSeq is re-executed, never carried over",
28363
+ "explicitId": null,
28364
+ "citations": [
28365
+ {
28366
+ "section": "spec/v2/core/runs.md §Fork",
28367
+ "requirement": null,
28368
+ "interpolated": true
28369
+ },
28370
+ {
28371
+ "section": null,
28372
+ "requirement": null,
28373
+ "interpolated": true
28374
+ },
28375
+ {
28376
+ "section": null,
28377
+ "requirement": null,
28378
+ "interpolated": true
28379
+ }
28380
+ ]
28381
+ },
28358
28382
  {
28359
28383
  "id": "openwop.it.v2-run-fork-refusals.replay-overlay-400-a-fromseq-outside-the-log-422-fork-point-invalid-a-valid-repl",
28360
28384
  "file": "v2-run-fork-refusals.test.ts",
@@ -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": 515,
4
+ "files": 516,
5
5
  "v1": 445,
6
- "v2": 71
6
+ "v2": 72
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1399,6 +1399,9 @@
1399
1399
  "v2-run-diff-identical.test.ts": [
1400
1400
  2
1401
1401
  ],
1402
+ "v2-run-fork-prefix.test.ts": [
1403
+ 2
1404
+ ],
1402
1405
  "v2-run-fork-refusals.test.ts": [
1403
1406
  2
1404
1407
  ],
@@ -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.59",
4
+ "version": "2.0.0-rc.60",
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": "4b474e22774660b14a66fdb680fca1c9c4a7402517009f751facf5bb7de97faf",
13
- "api/v2/asyncapi.yaml": "af8558829c54d427de29cde3eb99e8aac0a4ede445aa68cd9b85440370e22896",
14
- "api/v2/openapi.yaml": "c81590887da0826f56f9ee8995f28a1f2054eb59e9cf8faf791a3ffaf5710787",
12
+ "api/seams-v2.yaml": "924663b683aeb5374ffbac82b7daab78e98a30794eccf2ac38fd88bbb0e32412",
13
+ "api/v2/asyncapi.yaml": "82ae49c3c446692c64fcebc7057542d2e36d1ebd3e70a577e22bedd5bccf9211",
14
+ "api/v2/openapi.yaml": "a06f82c9046bcc7f21638e5dcf83603957898e3fb687a9711bf2d7d47e9dad31",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -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": "36ed772c70e35139274debb759f218c8580474417c8fe96af7b1b5918d3bad21",
202
+ "spec/v1/core-standard-manifest.json": "2f4d9846f6167092189acb28db6c5035df6ebdd404ddf2e09ff5ebfb5c58b5a4",
203
203
  "spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
204
204
  "spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
205
205
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -230,8 +230,8 @@
230
230
  "spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
231
231
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
232
232
  "spec/v2/profiles.json": "bf38fc9c5ad32212385ad798c0744de33837ea80c0ef9bec86ecefb4ea914f27",
233
- "spec/v2/release.json": "aa06fce026ea767c43b8840b26eb61551436dc58dd33187d2e07a1c0df5af322",
233
+ "spec/v2/release.json": "21de6736f0658c0d652d87d260967660aeea460fd5defbb097eb748715c23a38",
234
234
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
235
235
  },
236
- "corpusCommit": "bd43e1d1a83d9ee3fccf78501104fe0ed77da095"
236
+ "corpusCommit": "7042d0481198995c594dd9d8dc47ce2b774c782a"
237
237
  }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * `spec/v2/core/replay.md` §Endpoint — the fork prefix boundary is EXCLUSIVE
3
+ * (suite 2.0.0, target major 2; unaided; gated on `replay`; two runs created).
4
+ *
5
+ * "Events with `sequence < fromSeq` are fixed history; events `>= fromSeq` are
6
+ * re-executed." A host that copies `sequence <= fromSeq` inherits one event too
7
+ * many and re-executes from one event too late.
8
+ *
9
+ * Why this file exists. Until rc.60 the only major-2 witness of that boundary
10
+ * was `v2-fork-a-v1-run`, which is driven through the conformance seam and sits
11
+ * on the seams floor — so a host that does not advertise the seams profile was
12
+ * never measured on it, and `v2-run-fork-refusals` (the unaided fork scenario)
13
+ * asserts only the refusals and never reads the forked log. A tier-1 host
14
+ * shipped `<= fromSeq` and passed every unaided major-2 scenario; it found the
15
+ * defect only when fixing 1-based sequence numbering made the off-by-one
16
+ * visible — two errors cancelling. The v1 scenarios that do check the prefix
17
+ * (`replay-fork`, `replay-fork-arbitrary`, `feedback-fork-not-copied`) are
18
+ * `majors: [1]` and never run here.
19
+ *
20
+ * The falsifiable shape, chosen to be race-free and implementation-agnostic.
21
+ * `fromSeq` is placed on a `node.completed` that is not the run's last, so the
22
+ * source's event AT `fromSeq` is one re-execution cannot honestly reproduce at
23
+ * that same sequence: an exclusive host restarts that node, and its event at
24
+ * `fromSeq` is the fresh `node.started`; an inclusive host inherited the
25
+ * `node.completed` and re-executes from `fromSeq + 1`, so its event at
26
+ * `fromSeq` is the source's, verbatim. Both logs are read after they settle.
27
+ *
28
+ * Measured on the reference host (exclusive) at `fromSeq: 4` of an 8-event
29
+ * `conformance-multi-node` log: source `4 node.completed b`, fork
30
+ * `4 node.started b`, fork 9 events to the source's 8 — the fork is LONGER,
31
+ * because node `b` had started inside the prefix and its completion is
32
+ * re-executed. Length is therefore not the discriminator and this file does not
33
+ * assert on it; the event at the boundary is.
34
+ *
35
+ * What this file deliberately does NOT assert: the CONTENT of the re-executed
36
+ * tail, determinism across two forks, timing, and effect re-fire suppression.
37
+ * Those are `replay.md` §Replay determinism and the seams floor's, witnessed in
38
+ * `replay-fork-arbitrary` (major 1) and `v2-effect-seam-no-refire` (seams).
39
+ *
40
+ * @see spec/v2/core/replay.md §Endpoint
41
+ * @see spec/v2/core/runs.md §Fork
42
+ */
43
+
44
+ import { describe, it, expect } from 'vitest';
45
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
46
+ import { v2Discovery, gateFamily } from '../lib/v2.js';
47
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
48
+ import { readErrorCode } from '../lib/error-envelope.js';
49
+ import { softSkip } from '../lib/soft-skip.js';
50
+ import { req } from '../lib/requirement-ids.js';
51
+
52
+ const ID = 'openwop.requirement.0170.fork-prefix-boundary';
53
+ const DOC = 'spec/v2/core/replay.md §Endpoint';
54
+ const MULTI = 'conformance-multi-node';
55
+ const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
56
+
57
+ interface Ev { readonly sequence?: unknown; readonly type?: unknown }
58
+
59
+ async function discovery(): Promise<Record<string, unknown> | null> { try { return await v2Discovery(); } catch { return null; } }
60
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> { try { return await fn(); } catch { return null; } }
61
+ const enc = (id: string): string => encodeURIComponent(id);
62
+
63
+ /** Every event of a run, in sequence order, read through the poll surface. */
64
+ async function logOf(runId: string): Promise<Ev[] | null> {
65
+ const res = await http(() => driver.get(`/runs/${enc(runId)}/events/poll?timeout=1&streamMode=debug`));
66
+ if (res === null || res.status !== 200) return null;
67
+ const events = (res.json as { events?: unknown } | undefined)?.events;
68
+ if (!Array.isArray(events)) return null;
69
+ return (events as Ev[]).filter((e) => typeof e.sequence === 'number').sort((a, b) => (a.sequence as number) - (b.sequence as number));
70
+ }
71
+
72
+ /** Poll until the run is terminal or the bound elapses; returns the last status seen. */
73
+ async function settle(runId: string, ms = 20_000): Promise<string | null> {
74
+ const deadline = Date.now() + ms;
75
+ let status: string | null = null;
76
+ while (Date.now() < deadline) {
77
+ const res = await http(() => driver.get(`/runs/${enc(runId)}`));
78
+ status = res?.status === 200 ? String((res.json as { status?: unknown } | undefined)?.status ?? '') : status;
79
+ if (status !== null && TERMINAL.has(status)) return status;
80
+ await new Promise((r) => setTimeout(r, 200));
81
+ }
82
+ return status;
83
+ }
84
+
85
+ describe('v2 run-fork-prefix (replay.md §Endpoint — the boundary is exclusive)', () => {
86
+ it('a replay fork inherits exactly [0, fromSeq): the event at fromSeq is re-executed, never carried over', async () => {
87
+ if (!(await discovery())) return softSkip('blocked', 'v2 discovery unreachable');
88
+ if (!(await gateFamily('replay'))) return softSkip('inapplicable', 'replay family not advertised (gate recorded under openwop.family.replay) — forkRun is gated on replay (runs.md §Surface)');
89
+ if (!isFixtureAdvertised(MULTI)) return softSkip('inapplicable', `${MULTI} fixture not advertised — no multi-event deterministic run to fork at a mid-log point`);
90
+
91
+ const created = await http(() => driver.post('/runs', { workflowId: MULTI }));
92
+ if (created === null || created.status !== 201) return softSkip('blocked', `POST /runs {workflowId: ${MULTI}} answered ${created?.status ?? 'no response'} ${readErrorCode(created?.json) ?? ''}`.trim());
93
+ const runId = (created.json as { runId?: unknown }).runId;
94
+ if (typeof runId !== 'string') return softSkip('blocked', 'the 201 carried no runId');
95
+
96
+ const sourceStatus = await settle(runId);
97
+ if (sourceStatus === null || !TERMINAL.has(sourceStatus)) return softSkip('blocked', `the source run did not settle within 20 s (last status: ${sourceStatus ?? 'unreadable'}) — a mid-log fork point cannot be chosen from an unfinished log`);
98
+ const source = await logOf(runId);
99
+ if (source === null) return softSkip('blocked', `GET /runs/{runId}/events/poll did not answer 200 with events[] for the source run`);
100
+
101
+ // The fork point: a `node.completed` that is not the run's last event, so
102
+ // the source's event AT fromSeq is one an honest re-execution cannot
103
+ // reproduce at that sequence (it restarts the node instead).
104
+ const seqs = source.map((e) => e.sequence as number);
105
+ const last = Math.max(...seqs);
106
+ const point = source.find((e) => e.type === 'node.completed' && (e.sequence as number) > 0 && (e.sequence as number) < last);
107
+ if (point === undefined) return softSkip('inapplicable', `no node.completed sits strictly inside the ${source.length}-event source log (types: ${[...new Set(source.map((e) => String(e.type)))].join(', ')}) — this host's log shape offers no fork point where an inherited row and a re-executed row are distinguishable`);
108
+ const fromSeq = point.sequence as number;
109
+
110
+ const fork = await http(() => driver.post(`/runs/${enc(runId)}:fork`, { mode: 'replay', fromSeq }));
111
+ if (fork === null) return softSkip('blocked', 'POST /runs/{runId}:fork unreachable (fetch failed)');
112
+ if (fork.status === 404) return softSkip('blocked', 'POST /runs/{runId}:fork answered 404 — forkRun is served by a host advertising replay (runs.md §Surface)');
113
+ expect(fork.status, req(ID, 'spec/v2/core/runs.md §Fork', `a replay fork at a sequence in the source log MUST answer 201 — got ${fork.status} ${readErrorCode(fork.json) ?? ''}`.trim())).toBe(201);
114
+ const forkId = (fork.json as { runId?: unknown }).runId;
115
+ if (typeof forkId !== 'string') return softSkip('blocked', 'the fork 201 carried no runId');
116
+
117
+ const forkStatus = await settle(forkId);
118
+ if (forkStatus === null || !TERMINAL.has(forkStatus)) return softSkip('blocked', `the fork did not settle within 20 s (last status: ${forkStatus ?? 'unreadable'}) — the inherited-prefix length is only comparable on a finished log`);
119
+ const forked = await logOf(forkId);
120
+ if (forked === null) return softSkip('blocked', 'GET /runs/{runId}/events/poll did not answer 200 with events[] for the fork');
121
+
122
+ // The prefix itself: every inherited row matches the source by sequence and
123
+ // type. This is the fixed-history claim, stated positively.
124
+ const inherited = forked.filter((e) => (e.sequence as number) < fromSeq);
125
+ expect(
126
+ inherited.map((e) => `${String(e.sequence)}:${String(e.type)}`),
127
+ req(ID, DOC, `the fork MUST inherit [0, ${fromSeq}) from the source verbatim by sequence and type`),
128
+ ).toEqual(source.filter((e) => (e.sequence as number) < fromSeq).map((e) => `${String(e.sequence)}:${String(e.type)}`));
129
+
130
+ // The boundary. The source's event at `fromSeq` is re-executed, not fixed
131
+ // history, so it MUST NOT appear at its own sequence in the fork. An
132
+ // inclusive host copies [0, fromSeq] and that is exactly what it shows.
133
+ const at = forked.find((e) => (e.sequence as number) === fromSeq);
134
+ if (at === undefined) return softSkip('blocked', `the fork's log has no event at sequence ${fromSeq} (last ${Math.max(...forked.map((e) => e.sequence as number))}) — the boundary is unobservable on this log`);
135
+ expect(
136
+ String(at.type),
137
+ req(ID, DOC, `the event at sequence ${fromSeq} is re-executed, not fixed history: the fork MUST NOT carry the source's ${String(point.type)} at that sequence — seeing it means the prefix was copied inclusively ([0, ${fromSeq}] instead of [0, ${fromSeq}))`),
138
+ ).not.toBe(String(point.type));
139
+ }, 60_000);
140
+ });