@openwop/openwop-conformance 2.42.5 → 2.42.7

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.
Files changed (123) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +3 -3
  3. package/coverage.md +1 -1
  4. package/dist/lib/soft-skip.js +13 -0
  5. package/dist/spec-artifacts.lock.json +2 -2
  6. package/package.json +2 -2
  7. package/requirements.json +430 -162
  8. package/scenario-majors.json +16 -3
  9. package/schemas/CORPUS-STAMP.json +64 -63
  10. package/src/lib/audit-checkpoint.ts +72 -0
  11. package/src/lib/era2-seed.ts +30 -0
  12. package/src/lib/ext-claims.ts +96 -0
  13. package/src/lib/soft-skip.ts +14 -0
  14. package/src/scenarios/a2a-peer-authority.test.ts +6 -13
  15. package/src/scenarios/a2a-task-roundtrip.test.ts +6 -34
  16. package/src/scenarios/a2a-version-negotiation.test.ts +6 -12
  17. package/src/scenarios/a2ui-surface-delta-transport.test.ts +23 -12
  18. package/src/scenarios/agent-channel-dispatch.test.ts +13 -4
  19. package/src/scenarios/agent-live-invocation-bracket.test.ts +1 -1
  20. package/src/scenarios/agent-manifest-runtime.test.ts +3 -0
  21. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -2
  22. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +2 -2
  23. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -2
  24. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -2
  25. package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +2 -2
  26. package/src/scenarios/append-ordering.test.ts +26 -21
  27. package/src/scenarios/artifact-auth.test.ts +3 -1
  28. package/src/scenarios/artifact-type-registration-source.test.ts +2 -2
  29. package/src/scenarios/audit-checkpoint-signature.test.ts +63 -0
  30. package/src/scenarios/audit-checkpoint-vectors.test.ts +93 -0
  31. package/src/scenarios/auth-mtls.test.ts +7 -6
  32. package/src/scenarios/auth-subject-link-key-class.test.ts +3 -3
  33. package/src/scenarios/auth-subject-link.test.ts +19 -18
  34. package/src/scenarios/blob-presign-expiry.test.ts +2 -2
  35. package/src/scenarios/byok-roundtrip.test.ts +4 -1
  36. package/src/scenarios/compensation-behavior.test.ts +10 -5
  37. package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -0
  38. package/src/scenarios/connection-pack-apihosts.test.ts +2 -2
  39. package/src/scenarios/connection-provider-resolution.test.ts +2 -2
  40. package/src/scenarios/context-budget-transcript-bound.test.ts +7 -3
  41. package/src/scenarios/context-summarization-replay.test.ts +2 -0
  42. package/src/scenarios/cost-attribution.test.ts +3 -1
  43. package/src/scenarios/debugBundle.test.ts +12 -4
  44. package/src/scenarios/discovery.test.ts +6 -2
  45. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +2 -2
  46. package/src/scenarios/dispatch-input-mapping.test.ts +2 -2
  47. package/src/scenarios/dispatch-output-mapping.test.ts +15 -2
  48. package/src/scenarios/distillation-token-budget.test.ts +2 -2
  49. package/src/scenarios/durability-poison-exhaustion.test.ts +2 -2
  50. package/src/scenarios/envelope-nl-to-format-engaged.test.ts +2 -2
  51. package/src/scenarios/envelope-recovery-applied.test.ts +2 -2
  52. package/src/scenarios/envelope-refusal-shape.test.ts +2 -2
  53. package/src/scenarios/envelope-retry-attempted.test.ts +2 -2
  54. package/src/scenarios/envelope-retry-exhausted.test.ts +2 -2
  55. package/src/scenarios/envelope-tier-one-subset-static.test.ts +1 -1
  56. package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +2 -2
  57. package/src/scenarios/eventOrdering.test.ts +17 -10
  58. package/src/scenarios/form-content-instantiation.test.ts +2 -2
  59. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +2 -2
  60. package/src/scenarios/highConcurrency.test.ts +9 -9
  61. package/src/scenarios/idempotency-concurrent-claim.test.ts +5 -5
  62. package/src/scenarios/idempotency-key-determinism.test.ts +8 -8
  63. package/src/scenarios/idempotency.test.ts +6 -6
  64. package/src/scenarios/idempotencyRetry.test.ts +16 -11
  65. package/src/scenarios/interrupt-token-matrix.test.ts +24 -11
  66. package/src/scenarios/mcp-cache-tenant-scope.test.ts +3 -3
  67. package/src/scenarios/mcp-current-auth-boundary.test.ts +9 -6
  68. package/src/scenarios/mcp-extension-opacity.test.ts +6 -5
  69. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +9 -8
  70. package/src/scenarios/mcp-version-negotiation.test.ts +5 -10
  71. package/src/scenarios/media-url-inline-cap.test.ts +3 -2
  72. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -0
  73. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -2
  74. package/src/scenarios/memory-consolidation-idempotent.test.ts +2 -2
  75. package/src/scenarios/memory-injection-budget.test.ts +2 -2
  76. package/src/scenarios/multi-party-conversation-behavioral.test.ts +2 -2
  77. package/src/scenarios/multi-region-idempotency-behavior.test.ts +9 -9
  78. package/src/scenarios/multi-region-idempotency.test.ts +2 -2
  79. package/src/scenarios/pack-registry-isolation.test.ts +13 -12
  80. package/src/scenarios/pack-registry.test.ts +3 -2
  81. package/src/scenarios/pause-resume.test.ts +30 -11
  82. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +43 -34
  83. package/src/scenarios/provider-usage.test.ts +2 -2
  84. package/src/scenarios/redaction.test.ts +7 -5
  85. package/src/scenarios/redactionAdversarial.test.ts +5 -4
  86. package/src/scenarios/registry-public.test.ts +5 -1
  87. package/src/scenarios/replay-divergence-at-refusal.test.ts +10 -7
  88. package/src/scenarios/replay-fanout-suppression.test.ts +7 -4
  89. package/src/scenarios/replay-fork-arbitrary.test.ts +2 -2
  90. package/src/scenarios/replay-side-effect-suppression.test.ts +2 -2
  91. package/src/scenarios/replayDeterminism.test.ts +9 -4
  92. package/src/scenarios/runner-ledger.test.ts +4 -0
  93. package/src/scenarios/runtime-capabilities.test.ts +2 -0
  94. package/src/scenarios/speech-synthesis-roundtrip.test.ts +1 -1
  95. package/src/scenarios/streamReconnect.test.ts +2 -2
  96. package/src/scenarios/subrun-approval-gate.test.ts +2 -2
  97. package/src/scenarios/subworkflow-input-mapping.test.ts +2 -2
  98. package/src/scenarios/tool-catalog-projection.test.ts +10 -7
  99. package/src/scenarios/tool-session-lifecycle.test.ts +1 -1
  100. package/src/scenarios/trigger-ingestion.test.ts +2 -2
  101. package/src/scenarios/v2-a2ui-v09-surface.test.ts +15 -0
  102. package/src/scenarios/v2-effect-identity-business-key.test.ts +12 -0
  103. package/src/scenarios/v2-era-2-append-vocabulary.test.ts +39 -24
  104. package/src/scenarios/v2-ext-family-claims.test.ts +62 -0
  105. package/src/scenarios/v2-ext-rest-transport.test.ts +151 -0
  106. package/src/scenarios/v2-idempotency-in-flight.test.ts +3 -0
  107. package/src/scenarios/v2-legacy-profiles-absent.test.ts +4 -6
  108. package/src/scenarios/v2-mcp-tasks.test.ts +1 -1
  109. package/src/scenarios/v2-min-client-version.test.ts +2 -0
  110. package/src/scenarios/v2-refresh-sla.test.ts +6 -5
  111. package/src/scenarios/v2-sse-last-event-id-cursor.test.ts +3 -3
  112. package/src/scenarios/v2-sse-last-event-id.test.ts +2 -0
  113. package/src/scenarios/v2-version-header-honored.test.ts +3 -0
  114. package/src/scenarios/v2-webhook-durable-delivery.test.ts +2 -0
  115. package/src/scenarios/v2-webhook-secret-rotation.test.ts +8 -1
  116. package/src/scenarios/verifier-gating.test.ts +2 -2
  117. package/src/scenarios/version-fold.test.ts +2 -2
  118. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +7 -6
  119. package/src/scenarios/webhook-signed-delivery.test.ts +2 -0
  120. package/src/scenarios/workflow-chain-host-expansion.test.ts +10 -1
  121. package/src/scenarios/workflow-variable-format.test.ts +2 -2
  122. package/src/scenarios/workload-identity-chain-bounds.test.ts +6 -3
  123. package/vectors/audit-checkpoint-v1.json +260 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * v2-ext-rest-transport — RFC 0220 §C, the behavioral witness for the
3
+ * `restTransport` extension family (suite 2.42.7, target major 2).
4
+ *
5
+ * Gate: the host advertises `extensions["<org>.rest-transport"]` under a
6
+ * registered org (spec/v2/ext/restTransport/README.md §The claim). Absent ⇒
7
+ * `inapplicable`. A malformed claim fails the claim leg; it does not skip.
8
+ *
9
+ * Legs, both recorded under `openwop.family.restTransport` so the family row is
10
+ * the least certifiable of them (requirement-ledger fold):
11
+ *
12
+ * 1. `conditionalRunGet: true` — the `conformance-approval` fixture parks at
13
+ * `waiting-approval`, a stable state. The 200 carries a strong ETag, a
14
+ * matching If-None-Match gets 304 with no body, and once the approval is
15
+ * resolved and the run completes the ETag differs — a 304 across that
16
+ * transition would be stale (runs.md §"Caching and encoding").
17
+ * 2. `contentEncodings` — for each advertised coding, the response carries
18
+ * that `Content-Encoding` and decodes to the identity body byte for byte.
19
+ *
20
+ * A leg whose facet the record does not set records `inapplicable` for itself
21
+ * only: a host that claims one facet has promised nothing about the other.
22
+ *
23
+ * Ported from the v1 `run-transport-economy` scenario, which reads the v1 root
24
+ * key; this one reads the v2 extensions record.
25
+ *
26
+ * @see RFCS/0220-ext-families-graduate-on-evidence.md
27
+ * @see spec/v2/ext/restTransport/README.md
28
+ * @see spec/v2/core/runs.md §"Caching and encoding"
29
+ */
30
+ import { describe, it, expect } from 'vitest';
31
+ import { gunzipSync, brotliDecompressSync } from 'node:zlib';
32
+ import * as zlib from 'node:zlib';
33
+ import { driver } from '../lib/driver.js';
34
+ import { loadEnv } from '../lib/env.js';
35
+ import { v2Discovery } from '../lib/v2.js';
36
+ import { extDeclaration, classifyExtClaim } from '../lib/ext-claims.js';
37
+ import { pollUntilStatus, pollUntilTerminal } from '../lib/polling.js';
38
+ import { softSkip } from '../lib/soft-skip.js';
39
+ import { req } from '../lib/requirement-ids.js';
40
+
41
+ type Json = Record<string, unknown>;
42
+ type Encoding = 'gzip' | 'br' | 'zstd';
43
+ const APPROVAL_FIXTURE = 'conformance-approval';
44
+ const APPROVAL_NODE_ID = 'gate';
45
+ const NOOP_FIXTURE = 'conformance-noop';
46
+ const SECTION = 'spec/v2/ext/restTransport/README.md §What the claim adds';
47
+ const R = (why: string): string => req('openwop.family.restTransport', SECTION, why);
48
+
49
+ type Claim = { ok: true; record: Json; key: string } | { ok: false; kind: 'inapplicable' | 'blocked'; reason: string };
50
+
51
+ /** The one well-formed rest-transport record, or why there is none. A malformed claim is asserted here, not skipped. */
52
+ async function claim(): Promise<Claim> {
53
+ const decl = extDeclaration();
54
+ if (!decl) return { ok: false, kind: 'blocked', reason: 'spec/v2/declaration.json is not resolvable in this layout — the extension name and the org registry come from it' };
55
+ const family = decl.families.find((f) => f.key === 'restTransport');
56
+ if (!family) return { ok: false, kind: 'blocked', reason: 'the declaration has no ext row for restTransport — this scenario is stale against the corpus' };
57
+ const doc = await v2Discovery();
58
+ if (!doc) return { ok: false, kind: 'blocked', reason: 'v2 discovery unreachable' };
59
+ const claims = classifyExtClaim(doc, family, decl);
60
+ if (claims.length === 1 && claims[0].state === 'absent') return { ok: false, kind: 'inapplicable', reason: 'no extensions["<org>.rest-transport"] record under a registered org — the host does not advertise restTransport' };
61
+ for (const c of claims) {
62
+ if (c.state === 'malformed') expect(c.state, R(`extensions["${c.key}"]: ${c.why}`)).toBe('well-formed');
63
+ }
64
+ const first = claims.find((c) => c.state === 'well-formed');
65
+ if (!first || first.state !== 'well-formed') return { ok: false, kind: 'blocked', reason: 'no well-formed rest-transport claim' };
66
+ return { ok: true, record: ((doc['extensions'] as Json)[first.key] ?? {}) as Json, key: first.key };
67
+ }
68
+
69
+ function advertisedEncodings(record: Json): Encoding[] {
70
+ const raw = record['contentEncodings'];
71
+ return Array.isArray(raw) ? raw.filter((e): e is Encoding => e === 'gzip' || e === 'br' || e === 'zstd') : [];
72
+ }
73
+
74
+ describe('RFC 0220 §C — restTransport: conditional GET on the run snapshot', () => {
75
+ it('a strong ETag on the 200, 304 on a match while the run is parked, and a new ETag once it completes', async () => {
76
+ const c = await claim();
77
+ if (!c.ok) return softSkip(c.kind, c.reason);
78
+ if (c.record['conditionalRunGet'] !== true) return softSkip('inapplicable', `extensions["${c.key}"].conditionalRunGet is not true — the host claims no validator`);
79
+
80
+ const create = await driver.post('/runs', { workflowId: APPROVAL_FIXTURE });
81
+ // Any non-201 is the suite's precondition failing (fixture, credential), not the claim — the v2-a2ui gate's convention.
82
+ if (create.status !== 201) return softSkip('blocked', `POST /runs {workflowId: ${APPROVAL_FIXTURE}} answered ${create.status} — the run the leg needs could not be created`);
83
+ const runId = (create.json as { runId: string }).runId;
84
+ const path = `/runs/${encodeURIComponent(runId)}`;
85
+ await pollUntilStatus(runId, 'waiting-approval', { timeoutMs: 10_000 });
86
+
87
+ const parked = await driver.get(path);
88
+ expect(parked.status, R('the parked run reads 200')).toBe(200);
89
+ const etagParked = parked.headers.get('etag');
90
+ expect(etagParked, R('a host whose record sets conditionalRunGet: true MUST carry an ETag on every 200 of GET /runs/{runId}')).toBeTruthy();
91
+ expect(etagParked?.startsWith('W/'), R('the ETag MUST be strong')).toBe(false);
92
+
93
+ const revalidate = await driver.get(path, { headers: { 'If-None-Match': etagParked as string } });
94
+ expect(revalidate.status, R('a matching If-None-Match MUST receive 304 while the run has not advanced (runs.md §"Caching and encoding")')).toBe(304);
95
+ expect(revalidate.text, R('the 304 MUST carry no body')).toBe('');
96
+
97
+ const resolve = await driver.post(`${path}/interrupts/${encodeURIComponent(APPROVAL_NODE_ID)}`, { resumeValue: { action: 'accept' } });
98
+ expect(resolve.status, R('the approval resolves (control: the run must advance for the rotation check to mean anything)')).toBe(200);
99
+ const terminal = await pollUntilTerminal(runId, { timeoutMs: 10_000 });
100
+ expect(terminal.status, R('the run completes')).toBe('completed');
101
+
102
+ const done = await driver.get(path);
103
+ const etagDone = done.headers.get('etag');
104
+ expect(etagDone, R('the completed run still carries an ETag')).toBeTruthy();
105
+ expect(etagDone, R('the ETag MUST change once the run advances; the parked ETag would otherwise answer 304 for a changed run')).not.toBe(etagParked);
106
+ const stale = await driver.get(path, { headers: { 'If-None-Match': etagParked as string } });
107
+ expect(stale.status, R('the parked ETag MUST NOT match the completed run')).toBe(200);
108
+ });
109
+ });
110
+
111
+ describe('RFC 0220 §C — restTransport: each advertised coding round-trips', () => {
112
+ it('each coding in contentEncodings is produced when asked for alone and decodes to the identity body', async () => {
113
+ const c = await claim();
114
+ if (!c.ok) return softSkip(c.kind, c.reason);
115
+ const encodings = advertisedEncodings(c.record);
116
+ if (encodings.length === 0) return softSkip('inapplicable', `extensions["${c.key}"].contentEncodings lists no coding — the host claims none`);
117
+
118
+ const create = await driver.post('/runs', { workflowId: NOOP_FIXTURE });
119
+ // Any non-201 is the suite's precondition failing (fixture, credential), not the claim — the v2-a2ui gate's convention.
120
+ if (create.status !== 201) return softSkip('blocked', `POST /runs {workflowId: ${NOOP_FIXTURE}} answered ${create.status} — the run the leg needs could not be created`);
121
+ const runId = (create.json as { runId: string }).runId;
122
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
123
+
124
+ // A raw fetch, not the driver: the driver decodes JSON, and the oracle here is bytes.
125
+ const env = loadEnv();
126
+ const url = `${env.baseUrl}/runs/${encodeURIComponent(runId)}`;
127
+ const base = { Authorization: `Bearer ${env.apiKey}`, Accept: 'application/json', 'OpenWOP-Version': '2.0' };
128
+ const identity = await fetch(url, { headers: { ...base, 'Accept-Encoding': 'identity' } });
129
+ expect(identity.status, R('the identity read is 200')).toBe(200);
130
+ const identityBytes = Buffer.from(await identity.arrayBuffer());
131
+ expect(identityBytes.length, R('the identity body is non-empty')).toBeGreaterThan(0);
132
+
133
+ const zstdMaybe: { zstdDecompressSync?: (b: Buffer) => Buffer } = zlib;
134
+ for (const enc of encodings) {
135
+ const res = await fetch(url, { headers: { ...base, 'Accept-Encoding': enc } });
136
+ expect(res.status, R(`the ${enc} read is 200`)).toBe(200);
137
+ expect(res.headers.get('content-encoding'), R(`a coding listed in contentEncodings MUST be produced when it is the only one asked for (${enc})`)).toBe(enc);
138
+ expect((res.headers.get('vary') ?? '').toLowerCase(), R('a compressed response MUST carry Vary: Accept-Encoding (runs.md §"Caching and encoding")')).toContain('accept-encoding');
139
+ const bytes = Buffer.from(await res.arrayBuffer());
140
+ // Node's fetch decodes gzip and br in transit, so equal bytes already prove the round trip.
141
+ const decode = enc === 'gzip' ? gunzipSync : enc === 'br' ? brotliDecompressSync : zstdMaybe.zstdDecompressSync;
142
+ // No zstd decoder in this Node: the coding was produced (asserted above); the byte compare needs a decoder the suite lacks.
143
+ if (!bytes.equals(identityBytes) && !decode) continue;
144
+ let decoded = bytes;
145
+ if (!bytes.equals(identityBytes) && decode) {
146
+ try { decoded = Buffer.from(decode(bytes)); } catch { /* not compressed bytes either: a real mismatch, compared below */ }
147
+ }
148
+ expect(decoded.equals(identityBytes), R(`the ${enc}-decoded body MUST be byte-identical to the identity body`)).toBe(true);
149
+ }
150
+ });
151
+ });
@@ -147,6 +147,9 @@ describe('v2 idempotency-in-flight (idempotency.md Concurrency, RFC 0213 §B)',
147
147
  // (#1525's fix, kept verbatim; `r.refusals` is the split form's spelling of
148
148
  // its `refusals`, and it now lands on the LOSER id it is about rather than
149
149
  // on a row shared with the winner clause.)
150
+ // partial-witness-ok: the winner/replay clauses were asserted above; RFC 0213 §B lets every
151
+ // loser wait for a marked replay, so the 409 branch is an outcome this host did not take,
152
+ // not an unobserved obligation.
150
153
  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`);
151
154
  }, 60_000);
152
155
  // The deterministic §B witness (host-sample-test-seams.md §26): the seam only
@@ -41,11 +41,10 @@ describe('RFC 0175 §C.1 — legacy-profiles-absent (unaided)', () => {
41
41
  it('no -legacy id appears in a2a.profiles or mcp.profiles and each id matches its facet pattern', async () => {
42
42
  const doc = await discovery();
43
43
  if (!doc) return softSkip('blocked', 'discovery unreachable');
44
- let checked = 0;
45
- for (const family of ['a2a', 'mcp'] as const) {
46
- const profiles = profilesOf(doc, family);
47
- if (profiles === null) continue;
48
- checked++;
44
+ const families = (['a2a', 'mcp'] as const).filter((family) => profilesOf(doc, family) !== null);
45
+ if (families.length === 0) return softSkip('inapplicable', 'host advertises neither a2a nor mcp — no profiles[] to check for a -legacy id');
46
+ for (const family of families) {
47
+ const profiles = profilesOf(doc, family)!;
49
48
  for (const id of profiles) {
50
49
  expect(
51
50
  /-legacy$/.test(id),
@@ -64,7 +63,6 @@ describe('RFC 0175 §C.1 — legacy-profiles-absent (unaided)', () => {
64
63
  req('openwop.requirement.0175.legacy-profiles-absent', `facets/${family}.schema.json`, `the ${family} facet MUST validate against its facet schema (capabilities.schema.json ${family}): ${check.errors}`),
65
64
  ).toBe(true);
66
65
  }
67
- if (checked === 0) softSkip('inapplicable', 'host advertises neither a2a nor mcp — no profiles[] to check for a -legacy id');
68
66
  });
69
67
 
70
68
  it('no root profiles[] exists in the v2 representation', async () => {
@@ -309,7 +309,7 @@ describe('RFC 0198 — v2-mcp-tasks (MCP Tasks on the server mount; disconnect c
309
309
  if (f.result?.['resultType'] === 'task') terminal = await pollTask(m.url, String(f.result['taskId']), (s) => ['completed', 'failed', 'cancelled'].includes(s), 10_000);
310
310
  else {
311
311
  // unfailable-leg audit wave 2, 2026-09-27: this branch returned
312
- // softSkip('inapplicable') after the input_required asserts — a
312
+ // an inapplicable soft-skip after the input_required asserts — a
313
313
  // partial-witness PASS at both majors even when the synchronous answer
314
314
  // was NOT a tool-error outcome. The synchronous CallToolResult of a
315
315
  // failed run MUST still carry isError true; the task projection itself
@@ -50,6 +50,8 @@ describe('v2 min-client-version (RFC 0172 §A.5 — gated on minClientVersion)',
50
50
  if (res.status !== 426 && (res.status < 200 || res.status >= 300)) {
51
51
  return blockedDespiteAssertions(`the host advertises minClientVersion ${String(floor)} and answered ${res.status} to a client announcing 0.0.1 — neither served (the MAY) nor refused with the registered 426 client_version_unsupported; the refusal form was not observed`);
52
52
  }
53
+ // partial-witness-ok: the minClientVersion grammar MUST was asserted above; refusing a
54
+ // below-floor client is a MAY and this host served it, so the 426 shape binds nothing.
53
55
  if (res.status !== 426) return softSkip('inapplicable', `the host advertises minClientVersion ${String(floor)} but served a client announcing 0.0.1 (${res.status}) — refusal is a MAY; nothing further is observable`);
54
56
  expect(readErrorCode(res.json), req('openwop.requirement.0172.min-client-version', DOC, 'a 426 refusal MUST carry client_version_unsupported')).toBe('client_version_unsupported');
55
57
  const r = v2Validator('error-envelope')(res.json);
@@ -38,12 +38,14 @@ describe('RFC 0175 §D.4 — refresh-sla (unaided; gated on a2a or mcp)', () =>
38
38
  { family: 'a2a', offered: 'versions', preferred: 'preferredVersion', floor: 'minimumVersion' },
39
39
  { family: 'mcp', offered: 'revisions', preferred: 'preferredVersion', floor: 'minimumRevision' },
40
40
  ];
41
- let checked = 0;
42
- const now = Date.now();
41
+ const advertised: Array<{ f: (typeof facets)[number]; rec: Record<string, unknown> }> = [];
43
42
  for (const f of facets) {
44
43
  const rec = await familyAdvertised(f.family);
45
- if (!rec) continue;
46
- checked++;
44
+ if (rec) advertised.push({ f, rec });
45
+ }
46
+ if (advertised.length === 0) return softSkip('inapplicable', 'host advertises neither a2a nor mcp — no refreshedAt to check');
47
+ const now = Date.now();
48
+ for (const { f, rec } of advertised) {
47
49
  const refreshedAt = rec['refreshedAt'];
48
50
  expect(
49
51
  typeof refreshedAt === 'string' && DATE.test(refreshedAt),
@@ -73,6 +75,5 @@ describe('RFC 0175 §D.4 — refresh-sla (unaided; gated on a2a or mcp)', () =>
73
75
  req('openwop.requirement.0175.refresh-sla', 'interop.md §The facets', `${f.family}.${f.floor} MUST be one of the offered ${f.offered}[] — a floor below every offer refuses nothing`),
74
76
  ).toContain(rec[f.floor]);
75
77
  }
76
- if (checked === 0) return softSkip('inapplicable', 'host advertises neither a2a nor mcp — no refreshedAt to check');
77
78
  });
78
79
  });
@@ -10,9 +10,9 @@
10
10
  * the caller is authorized to read the run: for a run the caller cannot read
11
11
  * the answer MUST be the one the host gives without the header.
12
12
  *
13
- * Off the core-standard floor: `v2-sse-last-event-id` is the floor scenario and
14
- * this file adds legs a committed claim was never measured against (rc.59
15
- * precedent). Promotion waits until the three bundle hosts are measured.
13
+ * On the core-standard floor since 2.42.7, beside `v2-sse-last-event-id`. It was
14
+ * held off until the three bundle hosts were measured (rc.59 precedent); all
15
+ * three recorded it executed-pass, two with partial-witness on the SHOULD leg only.
16
16
  *
17
17
  * Legs, on one completed noop run:
18
18
  * 1. a future id (last + 1000) ⇒ 200, zero frames, closed by the server — a
@@ -92,6 +92,8 @@ describe('v2 sse-last-event-id (events.md §SSE frames)', () => {
92
92
  // values: resumption MUST emit a state.snapshot first — only where the host serves values.
93
93
  const probe = await http(() => driver.get(`/runs/${enc(c.runId)}/events?streamMode=bogus`, { headers: { Accept: 'text/event-stream' } }));
94
94
  const supported = (probe?.json as { details?: { supported?: unknown } } | null)?.details?.supported;
95
+ // partial-witness-ok: every Last-Event-ID MUST was asserted above on the debug stream; the
96
+ // state.snapshot-first rule binds only a host serving values mode.
95
97
  if (!Array.isArray(supported) || !supported.includes('values')) return softSkip('inapplicable', `values mode not served (details.supported: ${JSON.stringify(supported)}) — the state.snapshot-first resumption leg does not apply`);
96
98
  const values = await subscribe(`/runs/${enc(c.runId)}/events?streamMode=values`, { timeoutMs: 8_000, extraHeaders: V2, lastEventId: String(first) });
97
99
  expect(values.status, req(ID, DOC, `a values resume MUST answer 200 — got ${values.status}`)).toBe(200);
@@ -168,6 +168,9 @@ describe('v2-version-header-honored (RFC 0172 §A.3)', () => {
168
168
  },
169
169
  req(ID, DOC, `a host that does not serve major 2 MUST refuse OpenWOP-Version: 2.0 with 406 protocol_version_unsupported and details.protocolVersions[] echoing [${advertised.join(', ')}]; a host that LISTS a 2.x member MUST serve it (§1.3 row 1), not refuse it`),
170
170
  ).toEqual({ status: 406, code: 'protocol_version_unsupported', echoed: [...advertised].sort(), listsMajor2: false });
171
+ // partial-witness-ok: the 406 refusal was held to §1.3 row 2 in full above. It is a
172
+ // complete, correct answer, and there is no second representation to compare bytes
173
+ // against.
171
174
  return softSkip('inapplicable', 'the host refused major 2 with the specified 406 — a correct answer, and there is no second representation to compare bytes against');
172
175
  }
173
176
  if (asked.status !== 200) {
@@ -366,6 +366,8 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
366
366
  }
367
367
  }
368
368
  } else {
369
+ // partial-witness-ok: delivery and retry were observed above; the spacing leg binds only
370
+ // an advertised backoff other than none.
369
371
  softSkip('inapplicable', 'no advertised retryPolicy.backoff other than none — the spacing leg is not asserted');
370
372
  }
371
373
 
@@ -117,7 +117,14 @@ describe('RFC 0201 §E — secret rotation overlaps, then retires (gated on webh
117
117
  // After the overlap: only when it fits the operator's wait cap.
118
118
  const expiresAt = Date.parse(body.previousSecretExpiresAt ?? '');
119
119
  if (expiresAt - Date.now() > CAP_MS) {
120
- return softSkip('inapplicable', `the post-overlap leg is not observed: overlapSeconds (${overlapSeconds}) exceeds the suite wait cap (${CAP_MS}ms; raise OPENWOP_WEBHOOK_RETRY_WAIT_MS to witness it)`);
120
+ // partial-witness-ok: the tenant check, the rotate response and the dual signature during
121
+ // the overlap were asserted above. "The previous secret MUST NOT sign after
122
+ // previousSecretExpiresAt" cannot be observed inside a bounded run when the host's
123
+ // advertised webhooks.secretRotation.overlapSeconds outlasts the suite's wait cap, and
124
+ // blocking every long-overlap host would deny certification to conforming hosts
125
+ // (steward ruling, 2026-09-27). The row detail names both numbers. Raise
126
+ // OPENWOP_WEBHOOK_RETRY_WAIT_MS past the overlap to witness it.
127
+ return softSkip('inapplicable', `the post-overlap leg is not observed: the advertised webhooks.secretRotation.overlapSeconds is ${overlapSeconds}s (expiry ${new Date(expiresAt).toISOString()}) and the suite wait cap is ${CAP_MS}ms; raise OPENWOP_WEBHOOK_RETRY_WAIT_MS past the overlap to witness it`);
121
128
  }
122
129
  await new Promise((r) => setTimeout(r, Math.max(0, expiresAt - Date.now()) + 1_500));
123
130
  const after = await oneDelivery(rx, webhookId);
@@ -24,7 +24,7 @@ import { driver } from '../lib/driver.js';
24
24
  import { behaviorGate } from '../lib/behavior-gate.js';
25
25
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
26
26
  import { req } from '../lib/requirement-ids.js';
27
- import { softSkip } from '../lib/soft-skip.js';
27
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
28
28
 
29
29
  const SEAM = '/v1/host/sample/agents/verify-run';
30
30
 
@@ -62,7 +62,7 @@ describe('verifier-gating (RFC 0090 §B)', () => {
62
62
 
63
63
  // PASS verdict → completes normally.
64
64
  const passRes = await driver.post(SEAM, { simulateVerdict: 'pass' });
65
- if (passRes.status === 404) return softSkip('blocked', 'precondition not met — `passRes.status === 404` returned early (seam, prior step, or fixture unavailable)');
65
+ if (passRes.status === 404) return blockedDespiteAssertions('the verify seam answered the fail verdict but not the pass verdict — pass-completes is unobserved');
66
66
  expect(
67
67
  verifiedVerdict(passRes.json) === 'pass',
68
68
  req('openwop.it.verifier-gating.a-fail-verdict-blocks-commit-on-a-gating-host-a-pass-verdict-completes', 'RFC 0090 §A', 'a verify-run forcing a pass MUST emit agent.verified{verdict:"pass"}'),
@@ -34,7 +34,7 @@
34
34
  */
35
35
 
36
36
  import { describe, it, expect } from 'vitest';
37
- import { softSkip } from '../lib/soft-skip.js';
37
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
38
38
  import { driver, type OpenWOPResponse } from '../lib/driver.js';
39
39
  import { pollUntilTerminal } from '../lib/polling.js';
40
40
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -106,7 +106,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
106
106
  if (create.status === 403 && errCode(create.json) === 'force_engine_version_forbidden') {
107
107
  // eslint-disable-next-line no-console
108
108
  console.warn('[version-fold] API key is production-scoped (403 force_engine_version_forbidden); skipping');
109
- return softSkip('blocked', '[version-fold] API key is production-scoped (403 force_engine_version_forbidden); skipping (create.status === 403 && errCode(create.json) === \'force_engine_version_forbidden\')');
109
+ return blockedDespiteAssertions('[version-fold] API key is production-scoped (403 force_engine_version_forbidden) — the advertised forceEngineVersionRange cannot be exercised; run with a test key');
110
110
  }
111
111
  expect(
112
112
  create.status,
@@ -65,12 +65,13 @@ describe('wasm-pack-invoke-suspended: suspend → resume round-trip', () => {
65
65
  'RFCS/0008-wasm-abi.md §D',
66
66
  "if a host doesn't implement WASM-driven suspends it MUST surface a recognizable code",
67
67
  )).toBe(true);
68
- return softSkip('blocked', 'precondition not met — `terminal.status === \'failed\'` returned early (seam, prior step, or fixture unavailable)');
68
+ // A failed run carrying the recognizable code IS one of the two conformant
69
+ // outcomes: the requirement was observed.
70
+ } else {
71
+ // Completed path: the reference pack never suspends. Asserting
72
+ // 'completed' confirms the host did not spuriously enter a
73
+ // waiting-* state.
74
+ expect(terminal.status).toBe('completed');
69
75
  }
70
-
71
- // Completed path: the reference pack never suspends. Asserting
72
- // 'completed' confirms the host did not spuriously enter a
73
- // waiting-* state.
74
- expect(terminal.status).toBe('completed');
75
76
  });
76
77
  });
@@ -390,6 +390,8 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
390
390
  // `X-openwop-*` counterpart and the signature verifies reading either.
391
391
  const dual = first.headers['openwop-signature'];
392
392
  if (dual === undefined) {
393
+ // partial-witness-ok: every X-openwop-* delivery MUST was asserted above; dual-emitting
394
+ // the OpenWOP-* family is an RFC 0165 §C.1 SHOULD.
393
395
  softSkip('inapplicable', 'host does not yet dual-emit the OpenWOP-* webhook header family (RFC 0165 §C.1 — SHOULD)');
394
396
  } else {
395
397
  for (const [neu, old] of [
@@ -169,7 +169,10 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
169
169
  const [fm, fn] = PACK.version.split('.').map(Number);
170
170
  const older = hm! < fm! || (hm === fm && hn! < fn!);
171
171
  expect(older, req('openwop.it.workflow-chain-host-expansion.positive-1-node-chain-expansion-matches-the-reference-library-for-the-bundled-pa', 'workflow-chain-host-expansion', `host bundles workflow-chain-sample ${body.packVersion}; this suite's fixture is ${PACK.version} — a host pack NEWER than the suite is unaccounted for`)).toBe(true);
172
- softSkip('blocked', `host bundles workflow-chain-sample ${body.packVersion} (suite fixture ${PACK.version}) — the RFC 0157 chains are absent there until its conformance pin catches up`);
172
+ // No note here (until 2026-09-27 a blocked soft-skip turned this leg into
173
+ // a partial-witness pass): the chain this leg expands is identical in the
174
+ // older pack, so the comparison below is complete. The RFC 0157 legs,
175
+ // whose chains an older pack lacks, record their own `blocked`.
173
176
  }
174
177
  expect(typeof body.expansionId).toBe('string');
175
178
  expect(body.expansionId.length).toBeGreaterThan(0);
@@ -321,6 +324,9 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
321
324
  // the sign flipped.
322
325
  expect(res.status >= 400, req('openwop.it.workflow-chain-host-expansion.rfc-0157-a-chain-carrying-an-unwind-policy-is-refused-with-capability-required-o', 'compensation.md §"Workflow policy"', 'a non-advertising host MUST refuse a chain carrying `compensation` (policy) — 4xx, not 200')).toBe(true);
323
326
  expect(readErrorCode(res.json), req('openwop.it.workflow-chain-host-expansion.rfc-0157-a-chain-carrying-an-unwind-policy-is-refused-with-capability-required-o', 'capabilities.md §"Unsupported capability — refusal contract"', '`capability_required`')).toBe('capability_required');
327
+ // partial-witness-ok: the half of this requirement that binds a
328
+ // non-advertising host — refuse with capability_required — was observed
329
+ // above; the expand-on-an-advertiser half does not apply to this host.
324
330
  return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!advertises` returned early');
325
331
  }
326
332
  expect(res.status, req('openwop.it.workflow-chain-host-expansion.rfc-0157-a-chain-carrying-an-unwind-policy-is-refused-with-capability-required-o', 'workflow-chain-packs.md §"Compensation (RFC 0157)"', 'an advertising host expands the policy-carrying chain')).toBe(200);
@@ -338,6 +344,9 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
338
344
  if (body.settings?.compensation !== undefined) {
339
345
  expect(body.settings.compensation, req('openwop.it.workflow-chain-host-expansion.rfc-0157-a-chain-carrying-an-unwind-policy-is-refused-with-capability-required-o', 'workflow-chain-packs.md §"Compensation (RFC 0157)" rule 9b', 'the chain policy becomes settings.compensation, copied verbatim')).toEqual(expected.settingsCompensation);
340
346
  } else {
347
+ // partial-witness-ok: the node carry was observed above; echoing
348
+ // `settings` on the expand seam is host-optional, so the rule 9b policy
349
+ // carry is unobservable through it on a conforming host.
341
350
  softSkip('blocked', 'the expand seam does not echo `settings`, so the policy → settings.compensation carry (rule 9b) is unobservable through it; the node carry above was witnessed');
342
351
  }
343
352
  });
@@ -48,7 +48,7 @@ import { behaviorGate } from '../lib/behavior-gate.js';
48
48
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
49
49
  import { isFixtureAdvertised } from '../lib/fixtures.js';
50
50
  import { driver } from '../lib/driver.js';
51
- import { softSkip } from '../lib/soft-skip.js';
51
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
52
52
  import { req } from '../lib/requirement-ids.js';
53
53
  const WORKFLOW_DEF = join(SCHEMAS_DIR, 'workflow-definition.schema.json');
54
54
  // S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
@@ -193,7 +193,7 @@ describe('workflow-variable-format §B: host behaviour (RFC 0136, capability-gat
193
193
  if (res.status === 404) return softSkip('blocked', 'precondition not met — `res.status === 404` returned early (A host advertising deferredParameters but not yet serving the variables[]-returning seam extension soft-skips (404), as does a host with the seam disabled in…');
194
194
  expect(res.status, req('openwop.it.workflow-variable-format.b1-deferred-expansion-mints-format-onto-the-workflowvariable-string-copied-req-7', '§Deferred-parameter expansion', 'the deferred-expand seam returns 200')).toBe(200);
195
195
  const variables = (res.json as { variables?: Array<{ name: string; type?: string; format?: string }> }).variables;
196
- if (!Array.isArray(variables)) return softSkip('blocked', 'precondition not met — `!Array.isArray(variables)` returned early (seam present but not returning variables[] yet — soft-skip) (seam, prior step, or fixture unavailable)'); // seam present but not returning variables[] yet — soft-skip
196
+ if (!Array.isArray(variables)) return blockedDespiteAssertions('the deferred-expand seam answered 200 without variables[] — the minted WorkflowVariable.format is unobserved');
197
197
  const fmt = (n: string): string | undefined => variables.find((v) => v.name === n)?.format;
198
198
 
199
199
  expect(fmt('email'), req('openwop.it.workflow-variable-format.b1-deferred-expansion-mints-format-onto-the-workflowvariable-string-copied-req-7', '§WorkflowVariable', 'req 7: a string param\'s recognised `format` is minted verbatim')).toBe('email');
@@ -36,7 +36,7 @@ import { readErrorCode, readRetriable } from '../lib/error-envelope.js';
36
36
  import { driver } from '../lib/driver.js';
37
37
  import { behaviorGate } from '../lib/behavior-gate.js';
38
38
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
39
- import { seamAbsent, softSkip } from '../lib/soft-skip.js';
39
+ import { seamAbsent, softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
40
40
  import { req } from '../lib/requirement-ids.js';
41
41
 
42
42
  const DELEGATION_PROFILE = 'openwop-workload-identity-delegation';
@@ -94,7 +94,7 @@ describe('RFC 0154 §B — delegation chain bounds (capability-gated behavior)',
94
94
  const depth = caps.maxChainDepth as number;
95
95
  const chain = Array.from({ length: depth + 1 }, (_, i) => hop(i + 1));
96
96
  const r = await resolve({ identity: { ...IDENTITY, delegation: { chain, audience: 'openwop-host', expiresAt: LIVE } }, expectedAudience: 'openwop-host' });
97
- if (r === null) return softSkip('blocked', 'precondition not met — `r === null` returned early (seam, prior step, or fixture unavailable)');
97
+ if (r === null) return blockedDespiteAssertions('the identity-resolve seam is unwired — the maxChainDepth refusal is unobserved');
98
98
  expectRefusal('openwop.it.workload-identity-chain-bounds.a-chain-longer-than-the-advertised-maxchaindepth-is-refused', r, 'delegation_chain_too_long', `a chain of ${depth + 1} hops exceeds the advertised bound of ${depth} — each hop is another party the host trusts transitively`);
99
99
  });
100
100
 
@@ -113,7 +113,10 @@ describe('RFC 0154 §B — delegation chain bounds (capability-gated behavior)',
113
113
  expect(r.status >= 400, req('openwop.it.workload-identity-chain-bounds.a-chain-that-revisits-a-subject-is-refused-as-cyclic', 'RFCS/0154 §B', 'a cyclic chain is refused')).toBe(true);
114
114
  expect(['delegation_chain_cyclic', 'delegation_chain_too_long'], req('openwop.it.workload-identity-chain-bounds.a-chain-that-revisits-a-subject-is-refused-as-cyclic', 'spec/v1/host-sample-test-seams.md §20', 'cyclic or too-long — the bound is below the cycle length')).toContain(readErrorCode(r.json));
115
115
  expect(readRetriable(r.json)).toBe(false);
116
- return softSkip('blocked', 'precondition not met — `bound < 3` returned early (seam, prior step, or fixture unavailable)');
116
+ // partial-witness-ok: with maxChainDepth < 3 a 3-hop cyclic chain is also too long. The
117
+ // non-retriable refusal (either code) was asserted above; the cyclic-specific code cannot
118
+ // be told apart here.
119
+ return softSkip('inapplicable', 'maxChainDepth < 3: the cyclic chain is also too long, so the refusal was checked for either code');
117
120
  }
118
121
  expectRefusal('openwop.it.workload-identity-chain-bounds.a-chain-that-revisits-a-subject-is-refused-as-cyclic', r, 'delegation_chain_cyclic', 'a subject appearing twice is a chain that loops back through authority it already spent — unbounded laundering with a bounded length');
119
122
  });