@openwop/openwop-conformance 2.42.6 → 2.42.8

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 (129) hide show
  1. package/CHANGELOG.md +34 -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/fixtures/conformance-envelope-retry-attempted.json +2 -1
  7. package/package.json +2 -2
  8. package/requirements.json +581 -175
  9. package/scenario-majors.json +22 -3
  10. package/schemas/CORPUS-STAMP.json +70 -67
  11. package/src/lib/audit-checkpoint.ts +72 -0
  12. package/src/lib/ext-claims.ts +96 -0
  13. package/src/lib/mock-ai-budgets.ts +40 -0
  14. package/src/lib/soft-skip.ts +14 -0
  15. package/src/scenarios/a2a-peer-authority.test.ts +6 -13
  16. package/src/scenarios/a2a-task-roundtrip.test.ts +6 -34
  17. package/src/scenarios/a2a-version-negotiation.test.ts +6 -12
  18. package/src/scenarios/a2ui-surface-delta-transport.test.ts +23 -12
  19. package/src/scenarios/agent-channel-dispatch.test.ts +13 -4
  20. package/src/scenarios/agent-live-invocation-bracket.test.ts +1 -1
  21. package/src/scenarios/agent-manifest-runtime.test.ts +3 -0
  22. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -2
  23. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +2 -2
  24. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -2
  25. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -2
  26. package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +2 -2
  27. package/src/scenarios/append-ordering.test.ts +26 -21
  28. package/src/scenarios/artifact-auth.test.ts +3 -1
  29. package/src/scenarios/artifact-type-registration-source.test.ts +2 -2
  30. package/src/scenarios/audit-checkpoint-signature.test.ts +63 -0
  31. package/src/scenarios/audit-checkpoint-vectors.test.ts +93 -0
  32. package/src/scenarios/audit-log-integrity.test.ts +7 -16
  33. package/src/scenarios/auth-mtls.test.ts +7 -6
  34. package/src/scenarios/auth-subject-link-key-class.test.ts +3 -3
  35. package/src/scenarios/auth-subject-link.test.ts +19 -18
  36. package/src/scenarios/blob-presign-expiry.test.ts +2 -2
  37. package/src/scenarios/byok-roundtrip.test.ts +4 -1
  38. package/src/scenarios/compensation-behavior.test.ts +10 -5
  39. package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -0
  40. package/src/scenarios/connection-pack-apihosts.test.ts +2 -2
  41. package/src/scenarios/connection-provider-resolution.test.ts +2 -2
  42. package/src/scenarios/context-budget-transcript-bound.test.ts +7 -3
  43. package/src/scenarios/context-summarization-replay.test.ts +2 -0
  44. package/src/scenarios/cost-attribution.test.ts +3 -1
  45. package/src/scenarios/debugBundle.test.ts +12 -4
  46. package/src/scenarios/discovery.test.ts +6 -2
  47. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +2 -2
  48. package/src/scenarios/dispatch-input-mapping.test.ts +2 -2
  49. package/src/scenarios/dispatch-output-mapping.test.ts +15 -2
  50. package/src/scenarios/distillation-token-budget.test.ts +2 -2
  51. package/src/scenarios/durability-poison-exhaustion.test.ts +2 -2
  52. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +51 -15
  53. package/src/scenarios/envelope-nl-to-format-engaged.test.ts +2 -2
  54. package/src/scenarios/envelope-recovery-applied.test.ts +2 -2
  55. package/src/scenarios/envelope-refusal-shape.test.ts +2 -2
  56. package/src/scenarios/envelope-retry-attempted.test.ts +2 -2
  57. package/src/scenarios/envelope-retry-exhausted.test.ts +2 -2
  58. package/src/scenarios/envelope-tier-one-subset-static.test.ts +1 -1
  59. package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +2 -2
  60. package/src/scenarios/eventOrdering.test.ts +17 -10
  61. package/src/scenarios/form-content-instantiation.test.ts +2 -2
  62. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +2 -2
  63. package/src/scenarios/highConcurrency.test.ts +9 -9
  64. package/src/scenarios/idempotency-concurrent-claim.test.ts +5 -5
  65. package/src/scenarios/idempotency-key-determinism.test.ts +8 -8
  66. package/src/scenarios/idempotency.test.ts +6 -6
  67. package/src/scenarios/idempotencyRetry.test.ts +16 -11
  68. package/src/scenarios/interrupt-token-matrix.test.ts +24 -11
  69. package/src/scenarios/mcp-cache-tenant-scope.test.ts +3 -3
  70. package/src/scenarios/mcp-current-auth-boundary.test.ts +9 -6
  71. package/src/scenarios/mcp-extension-opacity.test.ts +6 -5
  72. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +9 -8
  73. package/src/scenarios/mcp-version-negotiation.test.ts +5 -10
  74. package/src/scenarios/media-url-inline-cap.test.ts +3 -2
  75. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -0
  76. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -2
  77. package/src/scenarios/memory-consolidation-idempotent.test.ts +2 -2
  78. package/src/scenarios/memory-injection-budget.test.ts +2 -2
  79. package/src/scenarios/multi-party-conversation-behavioral.test.ts +2 -2
  80. package/src/scenarios/multi-region-idempotency-behavior.test.ts +9 -9
  81. package/src/scenarios/multi-region-idempotency.test.ts +2 -2
  82. package/src/scenarios/pack-registry-isolation.test.ts +13 -12
  83. package/src/scenarios/pack-registry.test.ts +3 -2
  84. package/src/scenarios/pause-resume.test.ts +30 -11
  85. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +43 -34
  86. package/src/scenarios/provider-usage.test.ts +2 -2
  87. package/src/scenarios/redaction.test.ts +7 -5
  88. package/src/scenarios/redactionAdversarial.test.ts +5 -4
  89. package/src/scenarios/registry-public.test.ts +5 -1
  90. package/src/scenarios/replay-divergence-at-refusal.test.ts +10 -7
  91. package/src/scenarios/replay-fanout-suppression.test.ts +7 -4
  92. package/src/scenarios/replay-fork-arbitrary.test.ts +2 -2
  93. package/src/scenarios/replay-side-effect-suppression.test.ts +2 -2
  94. package/src/scenarios/replayDeterminism.test.ts +9 -4
  95. package/src/scenarios/runner-ledger.test.ts +4 -0
  96. package/src/scenarios/runtime-capabilities.test.ts +2 -0
  97. package/src/scenarios/speech-synthesis-roundtrip.test.ts +1 -1
  98. package/src/scenarios/streamReconnect.test.ts +2 -2
  99. package/src/scenarios/subrun-approval-gate.test.ts +2 -2
  100. package/src/scenarios/subworkflow-input-mapping.test.ts +2 -2
  101. package/src/scenarios/tool-catalog-projection.test.ts +10 -7
  102. package/src/scenarios/tool-session-lifecycle.test.ts +1 -1
  103. package/src/scenarios/trigger-ingestion.test.ts +2 -2
  104. package/src/scenarios/v2-a2ui-v09-surface.test.ts +15 -0
  105. package/src/scenarios/v2-client-version-header.test.ts +124 -0
  106. package/src/scenarios/v2-effect-identity-business-key.test.ts +12 -0
  107. package/src/scenarios/v2-ext-family-claims.test.ts +62 -0
  108. package/src/scenarios/v2-ext-rest-transport.test.ts +151 -0
  109. package/src/scenarios/v2-id-grammar.test.ts +17 -5
  110. package/src/scenarios/v2-idempotency-in-flight.test.ts +3 -0
  111. package/src/scenarios/v2-legacy-profiles-absent.test.ts +4 -6
  112. package/src/scenarios/v2-mcp-tasks.test.ts +1 -1
  113. package/src/scenarios/v2-min-client-version.test.ts +20 -14
  114. package/src/scenarios/v2-preferred-version-default.test.ts +19 -1
  115. package/src/scenarios/v2-refresh-sla.test.ts +6 -5
  116. package/src/scenarios/v2-sse-last-event-id-cursor.test.ts +3 -3
  117. package/src/scenarios/v2-sse-last-event-id.test.ts +2 -0
  118. package/src/scenarios/v2-version-header-honored.test.ts +3 -0
  119. package/src/scenarios/v2-webhook-durable-delivery.test.ts +2 -0
  120. package/src/scenarios/v2-webhook-generated-secret.test.ts +76 -0
  121. package/src/scenarios/v2-webhook-secret-rotation.test.ts +8 -1
  122. package/src/scenarios/verifier-gating.test.ts +2 -2
  123. package/src/scenarios/version-fold.test.ts +2 -2
  124. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +7 -6
  125. package/src/scenarios/webhook-signed-delivery.test.ts +2 -0
  126. package/src/scenarios/workflow-chain-host-expansion.test.ts +10 -1
  127. package/src/scenarios/workflow-variable-format.test.ts +2 -2
  128. package/src/scenarios/workload-identity-chain-bounds.test.ts +6 -3
  129. package/vectors/audit-checkpoint-v1.json +260 -0
@@ -50,15 +50,15 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §E — mcp-current-aut
50
50
  const { mcp, anon } = await disco();
51
51
  const claims = mcp?.supported === true && (mcp.profiles ?? []).includes(PROFILE) && mcp.serverMount?.supported === true;
52
52
  if (!behaviorGate(PROFILE, claims)) return;
53
+ // Anonymous access is permitted through the RFC 0132 surface when anonymousActor is
54
+ // advertised, so the refusal this leg checks does not apply. Known from discovery, so it
55
+ // is decided before any assertion (it used to follow the authenticated-call assert and
56
+ // record a partial-witness pass).
57
+ if (anon?.supported === true) return softSkip('inapplicable', 'anonymousActor is advertised — an anonymous current-profile request may be served through the RFC 0132 surface, so the refusal rule does not apply');
53
58
  const authed = await driver.post(await mcpServerMount(), REQ, { headers: HDR });
54
59
  if (authed.status === 404 || authed.status === 403) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${authed.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
55
60
  expect(authed.status, req('openwop.it.mcp-current-auth-boundary.an-unauthenticated-current-profile-request-is-refused-unless-anonymousactor-is-a', 'mcp-integration.md §E', 'the authenticated call MUST succeed at the same path, so a refusal below is not a wrong path')).toBe(200);
56
61
  const anonymous = await driver.post(await mcpServerMount(), REQ, { headers: HDR, authenticated: false });
57
- if (anon?.supported === true) {
58
- // Anonymous is permitted only through the RFC 0132 surface; a 200 here is that surface answering.
59
- expect([200, 401, 403]).toContain(anonymous.status);
60
- return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `anon?.supported === true` returned early');
61
- }
62
62
  expect(
63
63
  [401, 403],
64
64
  req('openwop.it.mcp-current-auth-boundary.an-unauthenticated-current-profile-request-is-refused-unless-anonymousactor-is-a', 'mcp-integration.md §E', 'an anonymous MCP principal MUST NOT be the production default for an advertised current profile — refuse (401/403) or advertise anonymousActor'),
@@ -74,7 +74,10 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §E — mcp-current-aut
74
74
  ...setCookies(anonymous.headers),
75
75
  ...setCookies((await driver.get('/.well-known/openwop', { authenticated: false })).headers),
76
76
  ];
77
- if (minted.length === 0) return softSkip('blocked', 'precondition not met — `minted.length === 0` returned early (host mints no anonymous session; the bare probe was the whole observation) (seam, prior step, or fixture unavailable)'); // host mints no anonymous session; the bare probe was the whole observation
77
+ // partial-witness-ok: the bare anonymous probe, the requirement, was refused and asserted
78
+ // above. The S30 replay binds only a host that mints an anonymous session, and this one
79
+ // minted none.
80
+ if (minted.length === 0) return softSkip('inapplicable', 'the host minted no anonymous session, so the bare-probe refusal asserted above was the whole observation');
78
81
  const cookie = minted.map((c) => c.split(';')[0]).join('; ');
79
82
  const withSession = await driver.post(await mcpServerMount(), REQ, { headers: { ...HDR, Cookie: cookie }, authenticated: false });
80
83
  expect(
@@ -54,12 +54,13 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §D — mcp-extension-o
54
54
  // an advertised-missing seam (RFC 0148 §B). A 403 is NOT a pass.
55
55
  return seamAbsent(`host advertises mcp-2026-07-28 but the invoke seam /v1/host/sample/mcp/invoke answered ${drive.status}`);
56
56
  }
57
- expect(server.invocations().some((i) => i.method === 'tools/call'), req('openwop.it.mcp-extension-opacity.client-half-an-extension-meta-asserting-authority-in-a-tool-result-changes-nothi', 'host-sample-test-seams.md §23', 'the host MUST have called the tool')).toBe(true);
57
+ // `extensionAuthority` is an OPTIONAL seam field (host-sample-test-seams.md §23). Its
58
+ // absence is `inapplicable`, decided before any assertion, never a failure or a
59
+ // `blocked` (the 2.42.6 appendEra2Event precedent). A report that IS present is held
60
+ // to the contract below.
58
61
  const rep = (drive.json as { extensionAuthority?: { scopesWidened?: boolean; approvalAdvanced?: boolean } }).extensionAuthority;
59
- if (rep === undefined) {
60
- expect(rep, req('openwop.it.mcp-extension-opacity.client-half-an-extension-meta-asserting-authority-in-a-tool-result-changes-nothi', 'host-sample-test-seams.md §23', 'the seam SHOULD report `extensionAuthority: { scopesWidened, approvalAdvanced }` for scenario "extension-asserts-authority"; until it does the requirement is `blocked`, not passed')).toBeDefined();
61
- return softSkip('blocked', 'precondition not met — `rep === undefined` returned early (seam, prior step, or fixture unavailable)');
62
- }
62
+ if (rep === undefined) return softSkip('inapplicable', 'the invoke seam reports no extensionAuthority (an optional seam field, host-sample-test-seams.md §23), so extension opacity is not observable on this host');
63
+ expect(server.invocations().some((i) => i.method === 'tools/call'), req('openwop.it.mcp-extension-opacity.client-half-an-extension-meta-asserting-authority-in-a-tool-result-changes-nothi', 'host-sample-test-seams.md §23', 'the host MUST have called the tool')).toBe(true);
63
64
  expect(rep.scopesWidened, req('openwop.it.mcp-extension-opacity.client-half-an-extension-meta-asserting-authority-in-a-tool-result-changes-nothi', 'mcp-integration.md §D', 'an extension MUST NOT gain tool authority or scope by appearing in _meta (mcp-extension-no-authority)')).toBe(false);
64
65
  expect(rep.approvalAdvanced, req('openwop.it.mcp-extension-opacity.client-half-an-extension-meta-asserting-authority-in-a-tool-result-changes-nothi', 'mcp-integration.md §D/§E', 'MCP content MUST NOT advance approval gates')).toBe(false);
65
66
  });
@@ -31,7 +31,7 @@
31
31
  */
32
32
 
33
33
  import { describe, it, expect } from 'vitest';
34
- import { softSkip, seamAbsent } from '../lib/soft-skip.js';
34
+ import { softSkip, seamAbsent, seamAbsentDespiteAssertions } from '../lib/soft-skip.js';
35
35
  import { driver } from '../lib/driver.js';
36
36
  import { mcpServerMount } from '../lib/mcp-mount.js';
37
37
  import { behaviorGate } from '../lib/behavior-gate.js';
@@ -72,19 +72,20 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §C — mcp-mrtr-roundt
72
72
  const calls = server.invocations().filter((i) => i.method === 'tools/call');
73
73
  expect(calls.length, req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'RFCS/0153 §C', 'the host MUST have called the tool at least once for this leg to mean anything')).toBeGreaterThan(0);
74
74
  const mrtr = (drive.json as { mrtr?: { inputRequiredSeen?: boolean; retried?: boolean; requestStateEchoed?: boolean; result?: unknown } }).mrtr;
75
- if (mrtr === undefined) {
76
- expect(mrtr, req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'host-sample-test-seams.md §23', 'the invoke seam SHOULD report `mrtr: { inputRequiredSeen, retried, requestStateEchoed, result }` for tool "needs_input"; until it does this requirement is unobservable and resolves to `blocked`')).toBeDefined();
77
- return softSkip('blocked', 'precondition not met — `mrtr === undefined` returned early (seam, prior step, or fixture unavailable)');
75
+ // `mrtr` is an OPTIONAL seam field (host-sample-test-seams.md §23). A host that omits
76
+ // it is not violating anything: the retry is still witnessed on the wire below, and
77
+ // only the seam's own report is skipped. A report that IS present is held to it.
78
+ if (mrtr !== undefined) {
79
+ expect(mrtr.inputRequiredSeen, req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'mcp-integration.md §C.1', 'the host MUST recognise resultType input_required')).toBe(true);
80
+ expect(mrtr.retried, req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'mcp-integration.md §C.1', 'the host MUST retry the ORIGINAL request with inputResponses (a live callback is not the current profile)')).toBe(true);
78
81
  }
79
- expect(mrtr.inputRequiredSeen, req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'mcp-integration.md §C.1', 'the host MUST recognise resultType input_required')).toBe(true);
80
- expect(mrtr.retried, req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'mcp-integration.md §C.1', 'the host MUST retry the ORIGINAL request with inputResponses (a live callback is not the current profile)')).toBe(true);
81
82
  // Cross-check from the wire: two tools/call, second carries requestState identical to what the server issued.
82
83
  expect(calls.length, req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'mcp-integration.md §C.1', 'initial + retry = two independent JSON-RPC requests on the wire')).toBeGreaterThanOrEqual(2);
83
84
  const retry = calls[calls.length - 1]!.params as { requestState?: string; inputResponses?: Record<string, unknown> };
84
85
  expect(typeof retry.requestState, req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'mcp-integration.md §C.1', 'requestState MUST be echoed exactly on the retry')).toBe('string');
85
86
  expect(retry.requestState?.startsWith('mrtr:needs_input:'), req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'mcp-integration.md §C.1', 'the echoed requestState is the value the server issued, byte-exact')).toBe(true);
86
87
  expect(retry.inputResponses?.['who'], req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'mcp-integration.md §C.1', 'inputResponses MUST answer every requested key')).toBeDefined();
87
- expect(mrtr.requestStateEchoed).toBe(true);
88
+ if (mrtr !== undefined) expect(mrtr.requestStateEchoed).toBe(true);
88
89
  for (const c of calls) {
89
90
  expect(c.revision, req('openwop.it.mcp-mrtr-roundtrip.input-required-gather-retry-with-inputresponses-echoed-requeststate-as-one-logic', 'mcp-integration.md §B', 'both calls MUST carry the current revision')).toBe('2026-07-28');
90
91
  }
@@ -123,7 +124,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §C — mcp-mrtr-roundt
123
124
  expect(reg.status, req('openwop.it.mcp-mrtr-roundtrip.a-suspending-tool-answers-input-required-with-elicitation-requeststate-the-retry', 'host-sample-test-seams.md §"sample workflows"', `the registration fixture MUST be accepted; a 4xx means the fixture this leg posts is malformed (suite defect): ${JSON.stringify(reg.json).slice(0, 200)}`)).toBeLessThan(400);
124
125
  const hdr = { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/call', 'Mcp-Name': TOOL };
125
126
  const first = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: TOOL, arguments: {}, _meta: { [META_V]: '2026-07-28', [META_C]: { elicitation: {} } } } }, { headers: hdr });
126
- if (first.status === 404) return seamAbsent('host advertises an MCP server mount but /v1/host/sample/mcp answered 404');
127
+ if (first.status === 404) return seamAbsentDespiteAssertions('host advertises an MCP server mount but /v1/host/sample/mcp answered 404 after the tool registered');
127
128
  const r1 = first.json as { result?: { resultType?: string; inputRequests?: Record<string, { method?: string }>; requestState?: string }; error?: { code: number } };
128
129
  expect(r1.error, req('openwop.it.mcp-mrtr-roundtrip.a-suspending-tool-answers-input-required-with-elicitation-requeststate-the-retry', 'mcp-integration.md §C.2', `tools/call MUST NOT error: ${JSON.stringify(r1.error)}`)).toBeUndefined();
129
130
  expect(r1.result?.resultType, req('openwop.it.mcp-mrtr-roundtrip.a-suspending-tool-answers-input-required-with-elicitation-requeststate-the-retry', 'mcp-integration.md §C.2', 'a run reaching waiting-input MUST answer the in-flight tools/call with resultType input_required (not a live callback)')).toBe('input_required');
@@ -88,16 +88,11 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
88
88
  server.reset();
89
89
  const drive = await driver.post('/v1/host/sample/mcp/invoke', { serverUrl: server.hostFacingEndpoint() });
90
90
  if (drive.status === 404 || drive.status === 403) {
91
- expect(
92
- drive.status,
93
- req('openwop.it.mcp-version-negotiation.outbound-calls-carry-mcp-protocol-version-in-date-form',
94
- 'RFCS/0153 §B',
95
- 'a host advertising MCP revisions MUST expose an invoke seam so the negotiated revision ' +
96
- 'is observable. Without it the requirement resolves to `blocked` per RFC 0148 §A — not ' +
97
- 'to a pass.',
98
- ),
99
- ).not.toBe(404);
100
- return softSkip('blocked', 'precondition not met — `drive.status === 404 || drive.status === 403` returned early (seam, prior step, or fixture unavailable)');
91
+ // A host advertising MCP revisions MUST expose the invoke seam so the negotiated
92
+ // version is observable. Default mode records `blocked` (RFC 0148 §A); strict mode
93
+ // fails (RFC 0148 §B). This was a not-404 assert followed by a blocked note, so a 403
94
+ // recorded a partial-witness pass at major 1.
95
+ return seamAbsent(`host advertises MCP revisions but the invoke seam /v1/host/sample/mcp/invoke answered ${drive.status}`);
101
96
  }
102
97
  const calls = server.invocations();
103
98
  expect(calls.length, req('openwop.it.mcp-version-negotiation.outbound-calls-carry-mcp-protocol-version-in-date-form', 'RFCS/0153 §B', 'the host MUST have called the server')).toBeGreaterThan(0);
@@ -148,7 +148,7 @@ describe.skipIf(HTTP_SKIP)('media-url-inline-cap: advertisement shape (RFC 0055
148
148
  const runIds = ((runs.json as { runs?: { runId?: string }[] }).runs ?? [])
149
149
  .map((r) => r.runId)
150
150
  .filter((id): id is string => typeof id === 'string');
151
- for (const runId of runIds) {
151
+ runs: for (const runId of runIds) {
152
152
  const bundle = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/debug-bundle`);
153
153
  if (bundle.status !== 200) continue;
154
154
  const events = (bundle.json as { events?: { type?: string; payload?: { url?: unknown; base64?: unknown } }[] }).events ?? [];
@@ -159,7 +159,8 @@ describe.skipIf(HTTP_SKIP)('media-url-inline-cap: advertisement shape (RFC 0055
159
159
  typeof ev.payload?.url === 'string' && ev.payload?.base64 === undefined,
160
160
  req('openwop.it.media-url-inline-cap.a-media-payload-in-a-run-debug-bundle-is-referenced-by-url-not-inlined-rfc-0055', 'ai-envelope.md §"Media reference payloads"', 'a media.* payload in a debug bundle MUST be a URL reference, never inlined binary'),
161
161
  ).toBe(true);
162
- return softSkip('blocked', 'precondition not met — `typeof ev.type === \'string\' && ev.type.startsWith(\'media.\')` returned early (asserted one — contract proven) (seam, prior step, or fixture unavailable)'); // asserted one — contract proven
162
+ // One media.* payload asserted by URL: the §C rule-3 requirement was observed.
163
+ break runs;
163
164
  }
164
165
  }
165
166
  }
@@ -117,6 +117,8 @@ describe('memory-compaction-provenance-tag: compacted-from:<id> tag follows §C
117
117
  if (provenance === undefined) {
118
118
  // eslint-disable-next-line no-console
119
119
  console.warn('[rfc0012-tag] output entry has no compacted-from:<id> tag — RFC 0012 §C is SHOULD, not MUST; pass with warning');
120
+ // partial-witness-ok: RFC 0012 §C tagging is a SHOULD. An untagged entry is this leg's
121
+ // "omits it cleanly" branch, and the entry was located and inspected above.
120
122
  return softSkip('inapplicable', '[rfc0012-tag] output entry has no compacted-from:<id> tag — RFC 0012 §C is SHOULD, not MUST; pass with warning');
121
123
  }
122
124
  expect(provenance, req('openwop.it.memory-compaction-provenance-tag.compacted-entry-carries-a-well-formed-compacted-from-tag-or-omits-it-cleanly-no',
@@ -27,7 +27,7 @@
27
27
  */
28
28
 
29
29
  import { describe, it, expect } from 'vitest';
30
- import { softSkip } from '../lib/soft-skip.js';
30
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
31
31
  import { driver } from '../lib/driver.js';
32
32
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
33
33
  import { req } from '../lib/requirement-ids.js';
@@ -101,7 +101,7 @@ describe('memory-compaction-sr1-carry-forward: derived content passes the BYOK r
101
101
  if (typeof event.outputContent !== 'string') {
102
102
  // eslint-disable-next-line no-console
103
103
  console.warn('[rfc0012-sr1] test seam did not return outputContent; the wire-level memory.compacted shape does not surface content so without a host-side seam we cannot verify §D end-to-end. Skipping.');
104
- return softSkip('blocked', '[rfc0012-sr1] test seam did not return outputContent; the wire-level memory.compacted shape does not surface content so without a host-side seam we cannot verify §D end-to-end. Skipping.');
104
+ return blockedDespiteAssertions('[rfc0012-sr1] the compact seam returned no outputContent, so the persisted entry\'s SR-1 carry-forward is unobserved');
105
105
  }
106
106
 
107
107
  // The load-bearing assertion: the PERSISTED entry content (what
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import { describe, it, expect } from 'vitest';
25
- import { softSkip } from '../lib/soft-skip.js';
25
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
26
26
  import { driver } from '../lib/driver.js';
27
27
  import { req } from '../lib/requirement-ids.js';
28
28
 
@@ -61,7 +61,7 @@ describe('memory-consolidation-idempotent: pass contract (RFC 0068 §D, capabili
61
61
  const second = await driver.post('/v1/host/sample/memory/consolidate', {
62
62
  memoryRef: 'mem://conformance/consolidation',
63
63
  });
64
- if (second.status === 404 || second.status === 501) return softSkip('blocked', 'precondition not met — `second.status === 404 || second.status === 501` returned early (seam, prior step, or fixture unavailable)');
64
+ if (second.status === 404 || second.status === 501) return blockedDespiteAssertions('the consolidate seam answered the first pass but not the second — §D.2 idempotence is unobserved');
65
65
  const r2 = second.json as ConsolidateResult;
66
66
  expect(
67
67
  r2.event?.inputCount,
@@ -116,7 +116,7 @@ describe('memory-injection-budget (RFC 0113)', () => {
116
116
 
117
117
  const v = await driveFixtureVariables();
118
118
  expect(v, req('openwop.it.memory-injection-budget.token-bounds-the-injection-read-omits-the-over-budget-entry-and-preserves-sr-1-c', 'RFC 0113', 'fixture MUST surface run variables')).toBeDefined();
119
- if (v === undefined) return softSkip('blocked', 'precondition not met — `v === undefined` returned early (seam, prior step, or fixture unavailable)');
119
+ if (v === undefined) throw new Error('unreachable: the assertion above has already failed this leg');
120
120
 
121
121
  // ── tokenBudget bound (the new lever) ──────────────────────────────
122
122
  const tokenBudget = numberOf(v['tokenBudget']);
@@ -177,7 +177,7 @@ describe('memory-injection-budget (RFC 0113)', () => {
177
177
 
178
178
  const v = await driveFixtureVariables();
179
179
  expect(v, req('openwop.it.memory-injection-budget.rank-relevance-reorders-vs-recency-only-when-memory-search-semantic-is-also-adve', 'RFC 0113', 'fixture MUST surface run variables')).toBeDefined();
180
- if (v === undefined) return softSkip('blocked', 'precondition not met — `v === undefined` returned early (seam, prior step, or fixture unavailable)');
180
+ if (v === undefined) throw new Error('unreachable: the assertion above has already failed this leg');
181
181
 
182
182
  const recencyOrder = stringArrayOf(v['recencyOrder']);
183
183
  const relevanceOrder = stringArrayOf(v['relevanceOrder']);
@@ -38,7 +38,7 @@
38
38
  */
39
39
 
40
40
  import { describe, it, expect } from 'vitest';
41
- import { seamAbsent } from '../lib/soft-skip.js';
41
+ import { seamAbsent, seamAbsentDespiteAssertions } from '../lib/soft-skip.js';
42
42
  import { behaviorGate } from '../lib/behavior-gate.js';
43
43
  import { isMultiPartyConversationSupported } from '../lib/multi-agent-capabilities.js';
44
44
  import {
@@ -89,7 +89,7 @@ describe('multi-party-conversation-behavioral (RFC 0101 §Conformance)', () => {
89
89
 
90
90
  // ---- MUST 1: POSITIVE — a roster-valid attributed turn is accepted ----
91
91
  const ok = await exchangeMultiPartyTurn({ conversationId: convId, turn: agentTurn('host:advisor-cfo', 1) });
92
- if (ok.unwired) return seamAbsent('host advertises multiPartyConversation but the conversation seam is not wired');
92
+ if (ok.unwired) return seamAbsentDespiteAssertions('host advertises multiPartyConversation and the council opened, but the turn-exchange seam is not wired');
93
93
  expect(
94
94
  ok.status === 200,
95
95
  req('openwop.it.multi-party-conversation-behavioral.opens-a-council-accepts-an-attributed-turn-and-rejects-missing-non-participant-o', 'RFC 0101 §Spec', "a role:'agent' turn with an in-roster speakerId MUST be accepted"),
@@ -87,14 +87,14 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
87
87
  expect(
88
88
  probe.status,
89
89
  req('openwop.it.multi-region-idempotency-behavior.two-region-conflict-resolves-to-the-lex-min-runid-per-annex-convergence-rule',
90
- 'idempotency.md §"Multi-region idempotency annex"',
90
+ 'spec/v1/idempotency.md §"Multi-region idempotency annex"',
91
91
  'simulate-partition seam MUST return 200 when ≥2 conflicting claims are submitted',
92
92
  ),
93
93
  ).toBe(200);
94
94
  expect(
95
95
  probe.body.winner?.runId,
96
96
  req('openwop.it.multi-region-idempotency-behavior.two-region-conflict-resolves-to-the-lex-min-runid-per-annex-convergence-rule',
97
- 'idempotency.md §"Convergence rule"',
97
+ 'spec/v1/idempotency.md §"Convergence rule"',
98
98
  'winner MUST be the lex-min runId (run-a-west < run-b-east)',
99
99
  ),
100
100
  ).toBe('run-a-west');
@@ -115,14 +115,14 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
115
115
  expect(
116
116
  probe.body.winner?.runId,
117
117
  req('openwop.it.multi-region-idempotency-behavior.three-region-partition-resolves-to-a-single-winner',
118
- 'idempotency.md §"Convergence rule"',
118
+ 'spec/v1/idempotency.md §"Convergence rule"',
119
119
  'winner MUST be the lex-min runId across all conflicting claims',
120
120
  ),
121
121
  ).toBe('aaa-1');
122
122
  expect(
123
123
  probe.body.losers?.length,
124
124
  req('openwop.it.multi-region-idempotency-behavior.three-region-partition-resolves-to-a-single-winner',
125
- 'idempotency.md §"Convergence rule"',
125
+ 'spec/v1/idempotency.md §"Convergence rule"',
126
126
  'losers array MUST contain N-1 entries when N claims conflict',
127
127
  ),
128
128
  ).toBe(2);
@@ -143,7 +143,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
143
143
  expect(
144
144
  redirects.length,
145
145
  req('openwop.it.multi-region-idempotency-behavior.every-region-gets-a-cache-redirect-entry-pointing-at-the-winner',
146
- 'idempotency.md §"Convergence rule"',
146
+ 'spec/v1/idempotency.md §"Convergence rule"',
147
147
  'cacheRedirects MUST contain one entry per claim (including the winner)',
148
148
  ),
149
149
  ).toBe(2);
@@ -151,7 +151,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
151
151
  expect(
152
152
  redirect.redirectToRunId,
153
153
  req('openwop.it.multi-region-idempotency-behavior.every-region-gets-a-cache-redirect-entry-pointing-at-the-winner',
154
- 'idempotency.md §"Convergence rule"',
154
+ 'spec/v1/idempotency.md §"Convergence rule"',
155
155
  'every cache redirect MUST point at the winner runId',
156
156
  ),
157
157
  ).toBe('run-a');
@@ -172,7 +172,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
172
172
  expect(
173
173
  probe.body.loserCancelReason,
174
174
  req('openwop.it.multi-region-idempotency-behavior.loser-cancel-reason-must-be-the-canonical-cross-region-dedup-loss-string',
175
- 'idempotency.md §"Convergence rule"',
175
+ 'spec/v1/idempotency.md §"Convergence rule"',
176
176
  'loserCancelReason MUST be the canonical `cross_region_dedup_loss` string',
177
177
  ),
178
178
  ).toBe('cross_region_dedup_loss');
@@ -198,7 +198,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
198
198
  expect(
199
199
  p1.body.winner?.runId,
200
200
  req('openwop.it.multi-region-idempotency-behavior.resolver-is-order-invariant-shuffled-inputs-produce-the-same-winner',
201
- 'idempotency.md §"Convergence rule" — determinism',
201
+ 'spec/v1/idempotency.md §"Convergence rule" — determinism',
202
202
  'resolver MUST be order-invariant; all permutations MUST produce the same lex-min winner',
203
203
  ),
204
204
  ).toBe('a');
@@ -219,7 +219,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
219
219
  expect(
220
220
  probe.status,
221
221
  req('openwop.it.multi-region-idempotency-behavior.mismatched-tuple-rejects-with-400-validation-error',
222
- 'idempotency.md §"Convergence rule"',
222
+ 'spec/v1/idempotency.md §"Convergence rule"',
223
223
  'claims with non-matching (tenantId, endpoint, key) MUST be rejected — it would be a programming error in the caller',
224
224
  ),
225
225
  ).toBe(400);
@@ -64,7 +64,7 @@ describe('multi-region-idempotency: capability shape', () => {
64
64
  }
65
65
 
66
66
  expect(ALLOWED.has(idem.crossRegion), req('openwop.it.multi-region-idempotency.idempotency-crossregion-when-advertised-must-be-one-of-the-closed-enum',
67
- 'idempotency.md §"Multi-region idempotency" §"Capability advertisement"',
67
+ 'spec/v1/idempotency.md §"Multi-region idempotency" §"Capability advertisement"',
68
68
  'crossRegion MUST be one of {"single-region","reconciled-records","fenced-effects"}',
69
69
  )).toBe(true);
70
70
 
@@ -90,7 +90,7 @@ describe('multi-region-idempotency: capability shape', () => {
90
90
  const advertised = new Set(observability?.metrics?.names ?? []);
91
91
  for (const name of REQUIRED_METRICS_WHEN_MULTI_REGION) {
92
92
  expect(advertised.has(name), req('openwop.it.multi-region-idempotency.multi-region-hosts-should-expose-the-cross-region-conflict-counter-per-operator',
93
- 'idempotency.md §"Operator surface"',
93
+ 'spec/v1/idempotency.md §"Operator surface"',
94
94
  `multi-region hosts SHOULD advertise metric "${name}" so operators can monitor conflict frequency`,
95
95
  )).toBe(true);
96
96
  }
@@ -94,18 +94,19 @@ describe('pack-registry-isolation: test catalog MUST NOT bleed into production (
94
94
  `pack name '${name}' was written via /v1/packs-test/${name}@${version} but appeared in /v1/packs/${name} response body — test-catalog isolation MUST hold`,
95
95
  ),
96
96
  ).toBe(false);
97
- return softSkip('blocked', 'precondition not met — `prodRes.status === 200` returned early (404 is the canonical "not found" — exactly what isolation requires. 200 with a payload that does NOT name our pack would mean the host returned a listing…');
97
+ // A 200 that does not name the pack IS isolation holding: the requirement was observed.
98
+ // (This branch used to end in a blocked note, a partial-witness pass at major 1.)
99
+ } else {
100
+ // Acceptable: 4xx range (404 pack_not_found is the spec-canonical
101
+ // shape; 410/422 also fine — any "not present in production catalog"
102
+ // signal satisfies the invariant).
103
+ expect(
104
+ prodRes.status >= 400 && prodRes.status < 500,
105
+ req('openwop.it.pack-registry-isolation.a-pack-put-to-v1-packs-test-name-must-not-appear-in-get-v1-packs-name',
106
+ 'RFCS/0025-test-mode-registry-namespace.md §C point 1',
107
+ `expected production-namespace GET to return 4xx for a test-namespace-only pack '${name}', got ${prodRes.status}`,
108
+ ),
109
+ ).toBe(true);
98
110
  }
99
-
100
- // Acceptable: 4xx range (404 pack_not_found is the spec-canonical
101
- // shape; 410/422 also fine — any "not present in production catalog"
102
- // signal satisfies the invariant).
103
- expect(
104
- prodRes.status >= 400 && prodRes.status < 500,
105
- req('openwop.it.pack-registry-isolation.a-pack-put-to-v1-packs-test-name-must-not-appear-in-get-v1-packs-name',
106
- 'RFCS/0025-test-mode-registry-namespace.md §C point 1',
107
- `expected production-namespace GET to return 4xx for a test-namespace-only pack '${name}', got ${prodRes.status}`,
108
- ),
109
- ).toBe(true);
110
111
  });
111
112
  });
@@ -330,9 +330,10 @@ describe('pack-registry: keychain shape (when present)', () => {
330
330
  authenticated: false,
331
331
  });
332
332
  // 200 with `{keys: []}` and 404 are both spec-allowed (keychain is
333
- // optional; not every namespace publishes one).
333
+ // optional; not every namespace publishes one). A 404 is decided before
334
+ // any assertion, so the row is `inapplicable`, not a partial-witness pass.
335
+ if (res.status === 404) return softSkip('inapplicable', `${first} publishes no keychain (optional) — there are no key entries to check`);
334
336
  expect([200, 404]).toContain(res.status);
335
- if (res.status === 404) return softSkip('blocked', 'precondition not met — `res.status === 404` returned early (seam, prior step, or fixture unavailable)');
336
337
 
337
338
  const body = res.json as { keys?: unknown; namespace?: unknown } | undefined;
338
339
  expect(Array.isArray(body?.keys), req('openwop.it.pack-registry.get-v1-packs-name-keychain-returns-well-formed-key-entries-when-present',
@@ -52,6 +52,25 @@ async function cancel(runId: string): Promise<void> {
52
52
  await driver.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, { reason: 'conformance-cleanup' });
53
53
  }
54
54
 
55
+ /**
56
+ * A 404 on :pause / :resume. The routes are capability-gated
57
+ * (capabilities.md §`runs.pauseResume`), so a host that does not advertise the
58
+ * family is out of scope (`inapplicable`), and one that advertises it and 404s
59
+ * has made a claim the suite cannot check (`blocked`).
60
+ *
61
+ * Every leg reaches this before its first assertion — the run create is an
62
+ * early return, not an `expect` — so neither case records the partial-witness
63
+ * PASS that a note after the create assertion recorded until 2026-09-27. A
64
+ * host that serves the routes without advertising them still runs every leg.
65
+ */
66
+ async function routeAbsent(route: ':pause' | ':resume'): Promise<undefined> {
67
+ const disco = await driver.get('/.well-known/openwop');
68
+ const advertised = capabilityFamily<{ pauseResume?: { supported?: unknown } }>(disco.json, 'runs')?.pauseResume?.supported === true;
69
+ return advertised
70
+ ? softSkip('blocked', `${route} answered 404 on a host advertising runs.pauseResume.supported — the advertised route is not served`)
71
+ : softSkip('inapplicable', `${route} answered 404 and the host does not advertise runs.pauseResume (capabilities.md §runs.pauseResume) — pause/resume is out of scope for it`);
72
+ }
73
+
55
74
  async function eventTypes(runId: string): Promise<string[]> {
56
75
  const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`);
57
76
  const events = (res.json as { events?: unknown } | null)?.events;
@@ -70,7 +89,7 @@ describe.skipIf(SKIP)('pause/resume: running → paused → running → terminal
70
89
  // `running`, so 3 s is ample for the pause to land first.
71
90
  inputs: { delayMs: 3_000 },
72
91
  });
73
- expect(create.status).toBe(201);
92
+ if (create.status !== 201) return softSkip('blocked', `POST /v1/runs answered ${create.status} — no run to pause`);
74
93
  const runId = (create.json as { runId: string }).runId;
75
94
 
76
95
  await pollUntilStatus(runId, 'running', { timeoutMs: 10_000 });
@@ -84,7 +103,7 @@ describe.skipIf(SKIP)('pause/resume: running → paused → running → terminal
84
103
  });
85
104
  if (pause.status === 404) {
86
105
  await cancel(runId);
87
- return softSkip('blocked', 'precondition not met — `pause.status === 404` returned early ([pause-resume] host returned 404 for :pause — endpoint not implemented; skipping rest) (seam, prior step, or fixture unavailable)');
106
+ return routeAbsent(':pause');
88
107
  }
89
108
  expect(pause.status, req('openwop.it.pause-resume.pause-transitions-to-paused-resume-returns-the-run-to-running',
90
109
  DOC_PAUSE,
@@ -112,7 +131,7 @@ describe.skipIf(SKIP)('pause/resume: :resume on a non-paused run returns 409', (
112
131
  workflowId: FIXTURE!,
113
132
  inputs: { delayMs: 30_000 },
114
133
  });
115
- expect(create.status).toBe(201);
134
+ if (create.status !== 201) return softSkip('blocked', `POST /v1/runs answered ${create.status} — no run to pause`);
116
135
  const runId = (create.json as { runId: string }).runId;
117
136
 
118
137
  await pollUntilStatus(runId, 'running', { timeoutMs: 10_000 });
@@ -120,7 +139,7 @@ describe.skipIf(SKIP)('pause/resume: :resume on a non-paused run returns 409', (
120
139
  const resume = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:resume`, {});
121
140
  if (resume.status === 404) {
122
141
  await cancel(runId);
123
- return softSkip('blocked', 'precondition not met — `resume.status === 404` returned early (seam, prior step, or fixture unavailable)');
142
+ return routeAbsent(':resume');
124
143
  }
125
144
  expect(resume.status, req('openwop.it.pause-resume.resuming-a-running-not-paused-run-returns-409-with-details-runstatus',
126
145
  DOC_RESUME,
@@ -146,14 +165,14 @@ describe.skipIf(SKIP)('pause/resume: a second :pause is 409 without a matching I
146
165
  workflowId: FIXTURE!,
147
166
  inputs: { delayMs: 30_000 },
148
167
  });
149
- expect(create.status).toBe(201);
168
+ if (create.status !== 201) return softSkip('blocked', `POST /v1/runs answered ${create.status} — no run to pause`);
150
169
  const runId = (create.json as { runId: string }).runId;
151
170
  await pollUntilStatus(runId, 'running', { timeoutMs: 10_000 });
152
171
 
153
172
  const first = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, { drainPolicy: 'immediate' });
154
173
  if (first.status === 404) {
155
174
  await cancel(runId);
156
- return softSkip('blocked', 'precondition not met — `first.status === 404` returned early (seam, prior step, or fixture unavailable)');
175
+ return routeAbsent(':pause');
157
176
  }
158
177
  expect(first.status, req('openwop.it.pause-resume.pause-on-an-already-paused-run-returns-409-with-details-runstatus-paused-unless',
159
178
  DOC_PAUSE,
@@ -211,7 +230,7 @@ describe.skipIf(SKIP)('pause/resume: :pause on a terminal run returns 409', () =
211
230
  await pollUntilTerminal(runId, { timeoutMs: 10_000 });
212
231
 
213
232
  const pause = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, {});
214
- if (pause.status === 404) return softSkip('blocked', 'precondition not met — `pause.status === 404` returned early (seam, prior step, or fixture unavailable)');
233
+ if (pause.status === 404) return routeAbsent(':pause');
215
234
  expect(pause.status, req('openwop.it.pause-resume.pause-on-a-completed-cancelled-failed-run-must-return-409',
216
235
  DOC_PAUSE,
217
236
  ':pause on a terminal run MUST return 409',
@@ -235,7 +254,7 @@ describe.skipIf(SKIP)('pause/resume: drain-current-node lets the executing node
235
254
  workflowId: FIXTURE!,
236
255
  inputs: { delayMs: 3_000 },
237
256
  });
238
- expect(create.status).toBe(201);
257
+ if (create.status !== 201) return softSkip('blocked', `POST /v1/runs answered ${create.status} — no run to pause`);
239
258
  const runId = (create.json as { runId: string }).runId;
240
259
  await pollUntilStatus(runId, 'running', { timeoutMs: 10_000 });
241
260
 
@@ -245,7 +264,7 @@ describe.skipIf(SKIP)('pause/resume: drain-current-node lets the executing node
245
264
  });
246
265
  if (pause.status === 404) {
247
266
  await cancel(runId);
248
- return softSkip('blocked', 'precondition not met — `pause.status === 404` returned early (seam, prior step, or fixture unavailable)');
267
+ return routeAbsent(':pause');
249
268
  }
250
269
  expect(pause.status, req('openwop.it.pause-resume.under-drain-current-node-node-completed-precedes-run-paused-in-the-log',
251
270
  DOC_PAUSE,
@@ -281,7 +300,7 @@ describe.skipIf(SKIP)('pause/resume: :pause-during-suspend race', () => {
281
300
  return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(\'conformance-approval\')` returned early ([pause-resume] conformance-approval not advertised; skipping :pause-during-suspend race subtest)');
282
301
  }
283
302
  const create = await driver.post('/v1/runs', { workflowId: 'conformance-approval' });
284
- expect(create.status).toBe(201);
303
+ if (create.status !== 201) return softSkip('blocked', `POST /v1/runs answered ${create.status} — no run to pause`);
285
304
  const runId = (create.json as { runId: string }).runId;
286
305
  await pollUntilStatus(runId, 'waiting-approval', { timeoutMs: 10_000 });
287
306
 
@@ -290,7 +309,7 @@ describe.skipIf(SKIP)('pause/resume: :pause-during-suspend race', () => {
290
309
  });
291
310
  if (pause.status === 404) {
292
311
  await cancel(runId);
293
- return softSkip('blocked', 'precondition not met — `pause.status === 404` returned early (seam, prior step, or fixture unavailable)');
312
+ return routeAbsent(':pause');
294
313
  }
295
314
 
296
315
  // Either rejection (preferred) or stacked-pause is OK; silent override is not.