@openwop/openwop-conformance 2.0.0-rc.34 → 2.0.0-rc.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,7 +112,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
112
112
 
113
113
  ## What's Covered
114
114
 
115
- The current suite has 500 scenario files under `src/scenarios/`.
115
+ The current suite has 501 scenario files under `src/scenarios/`.
116
116
  - 2026-09-03 (suite `1.157.0 -> 1.158.0`, gap G17): NEW `idempotency-concurrent-claim.test.ts` — drives the new `host-sample-test-seams.md` §25 concurrent duplicate-delivery seam for the RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" atomic-claim MUST, which is unconditional and had no witness of any kind. Asserts every executor mints the SAME `logicalInvocationId` **before** asserting `delivered === 1` — without the identity check a host passes by minting different ids and never colliding, one effect because nothing raced. Not profile-gated and so not opt-out-able (the obligation is unconditional); an unmounted seam records `blocked`, which is not certifiable. Graduates `layer2-invocation-claim-atomic` reference-impl -> protocol.
117
117
  - 2026-08-19 (suite `1.137.0 → 1.138.0`): NEW `durability-poison-exhaustion.test.ts` — RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts what `failure-path.test.ts` cannot: not just that deterministically failing work reaches terminal, but that attempts STOP — counted on the log, re-counted after a scaled quiet window, asserted unchanged. A host still redelivering records more. Seam-gated on the existing event-log seam (`blocked` = unobservable, not unmet) and outside every profile floor.
118
118
  - 2026-08-19 (suite `1.136.15 → 1.137.0`): NEW `replay-fanout-suppression.test.ts` — capability-gated on `webhooks.supported`, **outside every profile floor**; witnesses `replay.md` §"Host-initiated fan-out is an external effect", which was the largest normative MUST NOT on the replay surface with no scenario and no SECURITY invariant. Three legs in ONE `it` against ONE receiver and ONE subscription — a positive control, the MUST NOT, and a `branch` boundary leg — because "no delivery arrived" passes identically when delivery never worked, so absence is asserted only after presence is proven on that exact wiring. A host with an SSRF guard correctly refuses the loopback receiver and records `blocked`: **unobservable, not unmet.**
@@ -457,7 +457,7 @@ Server-required (added in 1.7.0):
457
457
  | ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
458
458
  | **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
459
459
 
460
- Current source tree: 500 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
460
+ Current source tree: 501 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
461
461
 
462
462
  ## Remaining Gaps
463
463
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.0.0-rc.34",
4
- "stampSha256": "c03eb9f0be923daa42666d83532e9234ba49c75662ac868a2408ec79c52b2200"
3
+ "version": "2.0.0-rc.35",
4
+ "stampSha256": "4e9bee704fa7ad2d9b6fac7e744d0be0982f5f85fbf8bd4ba8b85a4a4792e586"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.0.0-rc.34",
3
+ "version": "2.0.0-rc.35",
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.34"
59
+ "@openwop/spec-artifacts": "2.0.0-rc.35"
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": 547,
6
- "tests": 2123,
7
- "withStableId": 2123,
5
+ "files": 548,
6
+ "tests": 2124,
7
+ "withStableId": 2124,
8
8
  "interpolatedTitles": 0,
9
9
  "explicitIds": 2057
10
10
  },
@@ -28014,6 +28014,30 @@
28014
28014
  }
28015
28015
  ]
28016
28016
  },
28017
+ {
28018
+ "id": "openwop.it.v2-run-completed-outputs.the-run-completed-event-of-a-run-that-completed-carries-an-outputs-object",
28019
+ "file": "v2-run-completed-outputs.test.ts",
28020
+ "line": 72,
28021
+ "title": "the run.completed event of a run that completed carries an outputs object",
28022
+ "explicitId": null,
28023
+ "citations": [
28024
+ {
28025
+ "section": null,
28026
+ "requirement": "run.completed MUST carry a payload object",
28027
+ "interpolated": true
28028
+ },
28029
+ {
28030
+ "section": null,
28031
+ "requirement": null,
28032
+ "interpolated": true
28033
+ },
28034
+ {
28035
+ "section": null,
28036
+ "requirement": null,
28037
+ "interpolated": true
28038
+ }
28039
+ ]
28040
+ },
28017
28041
  {
28018
28042
  "id": "openwop.it.v2-subject-link-record.the-link-formed-across-scim-and-saml-is-a-closed-subjectlink-record",
28019
28043
  "file": "v2-subject-link-record.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": 500,
4
+ "files": 501,
5
5
  "v1": 445,
6
- "v2": 56
6
+ "v2": 57
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1375,6 +1375,9 @@
1375
1375
  "v2-revocation-honored.test.ts": [
1376
1376
  2
1377
1377
  ],
1378
+ "v2-run-completed-outputs.test.ts": [
1379
+ 2
1380
+ ],
1378
1381
  "v2-subject-link-record.test.ts": [
1379
1382
  2
1380
1383
  ],
@@ -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.34",
4
+ "version": "2.0.0-rc.35",
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": "72e297236e17ff0e67caa8abc6b7ac12853afaa90153044783574315f66e759f",
13
- "api/v2/asyncapi.yaml": "5e529b4a0f747b1685e1002711eaa90680ca5004c55a42f37c862d4a5e773cb7",
14
- "api/v2/openapi.yaml": "c61e5126577fa23ae6360c49fd77a2b1e3c1c088df8c73b02952048f331574c8",
12
+ "api/seams-v2.yaml": "f45e466353f491923d2766acbd32e9abc9d878bdfda20c18e002136d3c673a55",
13
+ "api/v2/asyncapi.yaml": "e51eb2f5ff9e19c40301e0fbe177e30018e81b7e872492a83abfe0a93be044f1",
14
+ "api/v2/openapi.yaml": "c06dd341cca4489abb0af934cfc292b7b91d3709f163dfd6c2dbf0be7522f4d5",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -169,7 +169,7 @@
169
169
  "schemas/v2/residency.schema.json": "af2658a7104cbcacd0e20a91226271c4a77f91ef46ae97612768fe743e6091d8",
170
170
  "schemas/v2/run-ancestry-response.schema.json": "907e36341ba83a2147a5d76963950b6dcd47506b663ecbccae9dfa553f3724d3",
171
171
  "schemas/v2/run-diff-response.schema.json": "6ab6cd265dc7e0a2086c6659c46aa0e7b43e6302949f0adac5d7deb1c7c0ecd3",
172
- "schemas/v2/run-event-payloads.schema.json": "6703e25b1f85a9993c466f9f84334f13142365ce329eaf182941c514e44fa0b9",
172
+ "schemas/v2/run-event-payloads.schema.json": "10f56762616535d01f411afdd98a70ecafc0a961f77aa68cf164b1da458464b4",
173
173
  "schemas/v2/run-event.schema.json": "1a9946374af9323b71635d00fed22abe02c300fa2cf3ae3e9c30f3e553e408ec",
174
174
  "schemas/v2/run-options.schema.json": "6d6052596ab0f17acfb00fdfc89907da7724f952e5ffbe1f81d94ba23d4c77cd",
175
175
  "schemas/v2/run-orchestrator-decided-event.schema.json": "bb76e15ff72e669f12938684033b55dc567470e1a378e19ffc3fa0e4034ab5a7",
@@ -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": "6d4ebbd50bc72c817ebdda8c4bf3a6ffe6bf5ea52bdd869d05fbc3662e69182f",
202
+ "spec/v1/core-standard-manifest.json": "822565b75e35d3874f5833bffbd7449f634cd02dab87bb1b3729d1db97a9d518",
203
203
  "spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
204
204
  "spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
205
205
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -230,7 +230,7 @@
230
230
  "spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
231
231
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
232
232
  "spec/v2/profiles.json": "1f0ee40491131cf561ef19a0394c47ba476b4567514fe16befb6cada4a10a5a4",
233
- "spec/v2/release.json": "06c40cd64441fc59fd3834191694da11b8bb0d631ad6f079bdc72047c44e67ca"
233
+ "spec/v2/release.json": "7da24ebcec492f8daab8786f6061be9280f31add5613a495325577726837b9b6"
234
234
  },
235
- "corpusCommit": "3d8b12f2b9364f89e5ede014ddd0024502d2311a"
235
+ "corpusCommit": "2653388c3bbf08347548414e78bc2df82a8f08d4"
236
236
  }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * `spec/v2/core/events.md` §Payloads — `run.completed` MUST carry `outputs`
3
+ * (suite 2.0.0, target major 2; unaided).
4
+ *
5
+ * This requirement is one day old, and the reason it needed writing is the
6
+ * reason this file exists. Both majors' payload schemas NAMED `outputs` on
7
+ * `runCompleted` and REQUIRED nothing. v1 also left the object open. So a
8
+ * tier-1 host emitted the singular `output` for its entire life and validated
9
+ * every time; v2 closing the object caught the EXTRA key — and that is the
10
+ * only reason anyone looked. Nothing in either major has ever caught an ABSENT
11
+ * one: a host emitting `run.completed {}` validates in v1 and, until rc.35, in
12
+ * v2. Seventeen scenario files mention `run.completed`; none read its payload.
13
+ *
14
+ * A key that no schema requires and no scenario reads is a requirement that
15
+ * exists only as a property name — the same shape as `eventLogSchemaVersion`,
16
+ * which lived in a schema `description` for the life of v1 while two hosts
17
+ * shipped without it. Asserting the EFFECT (the terminal event of a run that
18
+ * completed carries an outputs object) is the only form of this check a
19
+ * permissive emitter cannot satisfy by accident.
20
+ *
21
+ * @see spec/v2/core/events.md §Payloads
22
+ * @see schemas/v2/run-event-payloads.schema.json $defs/runCompleted
23
+ */
24
+
25
+ import { describe, it, expect } from 'vitest';
26
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
27
+ import { v2Discovery, v2Validator } from '../lib/v2.js';
28
+ import { readErrorCode } from '../lib/error-envelope.js';
29
+ import { softSkip } from '../lib/soft-skip.js';
30
+ import { req } from '../lib/requirement-ids.js';
31
+
32
+ const ID = 'openwop.requirement.0171.run-completed-outputs';
33
+ const DOC = 'spec/v2/core/events.md §Payloads';
34
+ const NOOP_WORKFLOW_ID = 'conformance-noop';
35
+
36
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
37
+ try { return await fn(); } catch { return null; }
38
+ }
39
+
40
+ async function createRun(): Promise<{ runId: string } | { reason: string }> {
41
+ try { if (!(await v2Discovery())) return { reason: 'v2 discovery unreachable' }; } catch { return { reason: 'v2 discovery unreachable' }; }
42
+ const res = await http(() => driver.post('/runs', { workflowId: NOOP_WORKFLOW_ID }));
43
+ if (res === null) return { reason: 'POST /runs unreachable (fetch failed)' };
44
+ const runId = (res.json as { runId?: unknown } | undefined)?.runId;
45
+ if (res.status !== 201 || typeof runId !== 'string') return { reason: `POST /runs {workflowId: ${NOOP_WORKFLOW_ID}} answered ${res.status} ${readErrorCode(res.json) ?? ''} — the smallest valid create was refused (fixture not seeded?)`.trim() };
46
+ return { runId };
47
+ }
48
+
49
+ interface Ev { readonly type?: unknown; readonly payload?: unknown }
50
+
51
+ /** Poll until the terminal event lands. The noop workflow completes in well under a second; the bound is for a slow host, not a slow workflow. */
52
+ async function terminalEvent(runId: string): Promise<{ ev: Ev } | { reason: string }> {
53
+ let last: string | null = null;
54
+ for (let i = 0; i < 20; i += 1) {
55
+ const res = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`));
56
+ if (res === null) return { reason: 'GET /runs/{runId}/events/poll unreachable (fetch failed)' };
57
+ if (res.status !== 200) return { reason: `GET /runs/{runId}/events/poll answered ${res.status}` };
58
+ const events = (res.json as { events?: unknown } | undefined)?.events;
59
+ if (Array.isArray(events)) {
60
+ const hit = events.find((e): e is Ev => typeof (e as Ev)?.type === 'string' && (e as Ev).type === 'run.completed');
61
+ if (hit) return { ev: hit };
62
+ const failed = events.find((e): e is Ev => typeof (e as Ev)?.type === 'string' && /^run\.(failed|cancelled)$/.test(String((e as Ev).type)));
63
+ if (failed) return { reason: `the noop run reached ${String(failed.type)} rather than run.completed — nothing to assert outputs on` };
64
+ last = `${events.length} event(s), none terminal`;
65
+ }
66
+ await new Promise((r) => setTimeout(r, 250));
67
+ }
68
+ return { reason: `run.completed never appeared (${last ?? 'no events'}) — the host may not run the noop workflow to completion` };
69
+ }
70
+
71
+ describe('v2 run-completed-outputs (events.md §Payloads)', () => {
72
+ it('the run.completed event of a run that completed carries an outputs object', async () => {
73
+ const c = await createRun();
74
+ if ('reason' in c) return softSkip('blocked', c.reason);
75
+ const t = await terminalEvent(c.runId);
76
+ if ('reason' in t) return softSkip('blocked', t.reason);
77
+
78
+ const payload = (t.ev.payload ?? null) as { outputs?: unknown; output?: unknown } | null;
79
+ expect(
80
+ payload !== null && typeof payload === 'object',
81
+ req(ID, DOC, 'run.completed MUST carry a payload object'),
82
+ ).toBe(true);
83
+ expect(
84
+ payload !== null && typeof payload.outputs === 'object' && payload.outputs !== null && !Array.isArray(payload.outputs),
85
+ req(ID, DOC, `run.completed MUST carry \`outputs\` as an object (an empty object is valid) — a client cannot tell "no outputs" from "outputs not rendered" when the key is absent, and no schema in either major required it until rc.35, which is how a host emitted the singular \`output\` for its whole life. Got keys ${JSON.stringify(payload ? Object.keys(payload) : null)}`),
86
+ ).toBe(true);
87
+
88
+ // The schema now requires it too; assert through the validator so the two
89
+ // cannot drift apart again without this leg saying which one moved.
90
+ const r = v2Validator('run-event')(t.ev);
91
+ expect(r.ok, req(ID, DOC, `the terminal event MUST validate against run-event.schema.json, whose runCompleted payload now requires outputs (${r.errors})`)).toBe(true);
92
+ });
93
+ });