@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
@@ -116,7 +116,7 @@ import { pollUntilTerminal } from '../lib/polling.js';
116
116
  import { isFixtureAdvertised } from '../lib/fixtures.js';
117
117
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
118
118
  import { req } from '../lib/requirement-ids.js';
119
- import { softSkip } from '../lib/soft-skip.js';
119
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
120
120
 
121
121
  const FIXTURE = 'conformance-envelope-retry-attempted';
122
122
  const NODE_ID = 'retry-attempted-structured-call';
@@ -165,7 +165,7 @@ describe.skipIf(HTTP_SKIP)('envelope-retry-attempted: runtime behavior (RFC 0032
165
165
  expect(seed.status).toBe(200);
166
166
 
167
167
  const events = await startRunAndRead();
168
- if (events === null) return softSkip('blocked', 'precondition not met — `events === null` returned early (seam, prior step, or fixture unavailable)');
168
+ if (events === null) return blockedDespiteAssertions('the advertised fixture run did not start (≠201) or its event log did not read (≠200) — envelope.retry.attempted is unobserved');
169
169
  const retries = events.filter((e) => e.type === 'envelope.retry.attempted');
170
170
  expect(
171
171
  retries.length,
@@ -29,7 +29,7 @@ import { driver } from '../lib/driver.js';
29
29
  import { pollUntilTerminal } from '../lib/polling.js';
30
30
  import { isFixtureAdvertised } from '../lib/fixtures.js';
31
31
  import { req } from '../lib/requirement-ids.js';
32
- import { softSkip } from '../lib/soft-skip.js';
32
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
33
33
 
34
34
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
35
35
  const FIXTURE = 'conformance-envelope-retry-exhausted';
@@ -86,7 +86,7 @@ describe.skipIf(HTTP_SKIP)('envelope-retry-exhausted: runtime behavior (RFC 0032
86
86
  expect(seed.status).toBe(200);
87
87
 
88
88
  const result = await startRunAndRead();
89
- if (result === null) return softSkip('blocked', 'precondition not met — `result === null` returned early (seam, prior step, or fixture unavailable)');
89
+ if (result === null) return blockedDespiteAssertions('the advertised fixture run did not start (≠201) or its event log did not read (≠200) — envelope.retry.exhausted is unobserved');
90
90
  const { events } = result;
91
91
  const exhausted = events.filter((e) => e.type === 'envelope.retry.exhausted');
92
92
  expect(
@@ -219,7 +219,7 @@ describe('envelope-tier-one-subset-static: universal-kind schemas satisfy load-b
219
219
  it(`${kind}.schema.json satisfies load-bearing Tier-1 rules (no oneOf/allOf/not/prefixItems/propertyNames anywhere)`, () => {
220
220
  const schema = loadLocalSchema(kind);
221
221
  expect(schema, req('openwop.it.envelope-tier-one-subset-static.schema-json-satisfies-load-bearing-tier-1-rules-no-oneof-allof-not-prefixitems-p', 'RFC 0030 §B', `schemas/envelopes/${kind}.schema.json MUST exist`)).not.toBeNull();
222
- if (schema === null) return softSkip('blocked', 'precondition not met — `schema === null` returned early (seam, prior step, or fixture unavailable)');
222
+ if (schema === null) throw new Error('unreachable: the assertion above has already failed this leg');
223
223
  const violations: Violation[] = [];
224
224
  const propCount = { n: 0 };
225
225
  walkSchema(schema, `#`, 0, propCount, violations, 'load-bearing');
@@ -20,7 +20,7 @@ import { driver } from '../lib/driver.js';
20
20
  import { pollUntilTerminal } from '../lib/polling.js';
21
21
  import { isFixtureAdvertised } from '../lib/fixtures.js';
22
22
  import { req } from '../lib/requirement-ids.js';
23
- import { softSkip } from '../lib/soft-skip.js';
23
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
24
24
 
25
25
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
26
26
  const FIXTURE = 'conformance-envelope-truncation-cap-exhaustion';
@@ -65,7 +65,7 @@ describe.skipIf(HTTP_SKIP)('envelope-truncation-cap-exhaustion: DoS-bound retry
65
65
  expect(seed.status).toBe(200);
66
66
 
67
67
  const result = await startRunAndRead();
68
- if (result === null) return softSkip('blocked', 'precondition not met — `result === null` returned early (seam, prior step, or fixture unavailable)');
68
+ if (result === null) return blockedDespiteAssertions('the advertised fixture run did not start (≠201) or its event log did not read (≠200) — the truncation-cap envelope.retry.exhausted is unobserved');
69
69
  const exhausted = result.events.filter((e) => e.type === 'envelope.retry.exhausted');
70
70
  expect(
71
71
  exhausted.length,
@@ -20,7 +20,7 @@ import { driver } from '../lib/driver.js';
20
20
  import { pollUntilTerminal } from '../lib/polling.js';
21
21
  import { isFixtureAdvertised } from '../lib/fixtures.js';
22
22
  import { req } from '../lib/requirement-ids.js';
23
- import { softSkip } from '../lib/soft-skip.js';
23
+ import { blockedDespiteAssertions } from '../lib/soft-skip.js';
24
24
 
25
25
  const NOOP_WORKFLOW_ID = 'conformance-noop';
26
26
  const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
@@ -42,6 +42,13 @@ function getSeq(event: RawEvent): number | null {
42
42
  return null;
43
43
  }
44
44
 
45
+ // Every early return below follows the run-create assertion, so each is
46
+ // `blockedDespiteAssertions`: rest-endpoints.md §"Required endpoints" makes
47
+ // /events/poll a MUST on every host, and observability.md makes run.started and
48
+ // exactly one terminal event MUSTs, so a completed noop run on a conforming host
49
+ // always yields a readable events[] of at least two. A host that does not was
50
+ // never observed ordering anything — until 2026-09-27 these recorded a
51
+ // partial-witness PASS.
45
52
  describe.skipIf(SKIP_NO_NOOP)('event-ordering: polling returns events in monotonic order', () => {
46
53
  it('events from a single poll have non-decreasing sequence numbers', async () => {
47
54
  const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
@@ -50,11 +57,11 @@ describe.skipIf(SKIP_NO_NOOP)('event-ordering: polling returns events in monoton
50
57
  await pollUntilTerminal(runId);
51
58
 
52
59
  const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
53
- if (res.status !== 200) return softSkip('blocked', 'precondition not met — `res.status !== 200` returned early (seam, prior step, or fixture unavailable)');
60
+ if (res.status !== 200) return blockedDespiteAssertions('precondition not met — `res.status !== 200` returned early (seam, prior step, or fixture unavailable)');
54
61
 
55
62
  const body = res.json as { events?: RawEvent[] } | undefined;
56
- if (!body?.events) return softSkip('blocked', 'precondition not met — `!body?.events` returned early (seam, prior step, or fixture unavailable)');
57
- if (body.events.length < 2) return softSkip('blocked', 'precondition not met — `body.events.length < 2` returned early (single-event runs have no ordering to verify) (seam, prior step, or fixture unavailable)'); // single-event runs have no ordering to verify
63
+ if (!body?.events) return blockedDespiteAssertions('precondition not met — `!body?.events` returned early (seam, prior step, or fixture unavailable)');
64
+ if (body.events.length < 2) return blockedDespiteAssertions('precondition not met — `body.events.length < 2` returned early (single-event runs have no ordering to verify) (seam, prior step, or fixture unavailable)'); // single-event runs have no ordering to verify
58
65
 
59
66
  const seqs = body.events.map(getSeq);
60
67
  for (let i = 1; i < seqs.length; i++) {
@@ -76,11 +83,11 @@ describe.skipIf(SKIP_NO_NOOP)('event-ordering: polling returns events in monoton
76
83
 
77
84
  const a = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
78
85
  const b = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
79
- if (a.status !== 200 || b.status !== 200) return softSkip('blocked', 'precondition not met — `a.status !== 200 || b.status !== 200` returned early (seam, prior step, or fixture unavailable)');
86
+ if (a.status !== 200 || b.status !== 200) return blockedDespiteAssertions('precondition not met — `a.status !== 200 || b.status !== 200` returned early (seam, prior step, or fixture unavailable)');
80
87
 
81
88
  const aBody = a.json as { events?: RawEvent[] } | undefined;
82
89
  const bBody = b.json as { events?: RawEvent[] } | undefined;
83
- if (!aBody?.events || !bBody?.events) return softSkip('blocked', 'precondition not met — `!aBody?.events || !bBody?.events` returned early (seam, prior step, or fixture unavailable)');
90
+ if (!aBody?.events || !bBody?.events) return blockedDespiteAssertions('precondition not met — `!aBody?.events || !bBody?.events` returned early (seam, prior step, or fixture unavailable)');
84
91
 
85
92
  expect(aBody.events.length, req('openwop.it.eventOrdering.repeated-polls-of-a-terminal-run-yield-identical-event-sequences',
86
93
  'observability.md',
@@ -107,10 +114,10 @@ describe.skipIf(SKIP_NO_NOOP)('event-ordering: terminal run has at most one term
107
114
  await pollUntilTerminal(runId);
108
115
 
109
116
  const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
110
- if (res.status !== 200) return softSkip('blocked', 'precondition not met — `res.status !== 200` returned early (seam, prior step, or fixture unavailable)');
117
+ if (res.status !== 200) return blockedDespiteAssertions('precondition not met — `res.status !== 200` returned early (seam, prior step, or fixture unavailable)');
111
118
 
112
119
  const body = res.json as { events?: RawEvent[] } | undefined;
113
- if (!body?.events) return softSkip('blocked', 'precondition not met — `!body?.events` returned early (seam, prior step, or fixture unavailable)');
120
+ if (!body?.events) return blockedDespiteAssertions('precondition not met — `!body?.events` returned early (seam, prior step, or fixture unavailable)');
114
121
 
115
122
  const TERMINAL_TYPES = new Set(['run.completed', 'run.failed', 'run.cancelled']);
116
123
  const terminalCount = body.events.filter((e) => typeof e.type === 'string' && TERMINAL_TYPES.has(e.type)).length;
@@ -128,10 +135,10 @@ describe.skipIf(SKIP_NO_NOOP)('event-ordering: terminal run has at most one term
128
135
  await pollUntilTerminal(runId);
129
136
 
130
137
  const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
131
- if (res.status !== 200) return softSkip('blocked', 'precondition not met — `res.status !== 200` returned early (seam, prior step, or fixture unavailable)');
138
+ if (res.status !== 200) return blockedDespiteAssertions('precondition not met — `res.status !== 200` returned early (seam, prior step, or fixture unavailable)');
132
139
 
133
140
  const body = res.json as { events?: RawEvent[] } | undefined;
134
- if (!body?.events || body.events.length === 0) return softSkip('blocked', 'precondition not met — `!body?.events || body.events.length === 0` returned early (seam, prior step, or fixture unavailable)');
141
+ if (!body?.events || body.events.length === 0) return blockedDespiteAssertions('precondition not met — `!body?.events || body.events.length === 0` returned early (seam, prior step, or fixture unavailable)');
135
142
 
136
143
  const TERMINAL_TYPES = new Set(['run.completed', 'run.failed', 'run.cancelled']);
137
144
  const lastEvent = body.events[body.events.length - 1]!;
@@ -65,7 +65,7 @@ import {
65
65
  PLAIN_TEXT_CONTROLS,
66
66
  } from '../lib/formContentPacks.js';
67
67
  import { req } from '../lib/requirement-ids.js';
68
- import { softSkip } from '../lib/soft-skip.js';
68
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
69
69
 
70
70
  /**
71
71
  * Conformance fixture templates a host wires the seam against.
@@ -140,7 +140,7 @@ describe('form-content-instantiation: a host instantiates a registered template
140
140
  ).toBe(true);
141
141
 
142
142
  const field = fieldById(res.fields, VENDOR_FIELD_ID);
143
- if (field === undefined) return softSkip('blocked', 'precondition not met — `field === undefined` returned early (host doesn\'t report per-field controls on the seam — soft-skip) (seam, prior step, or fixture unavailable)'); // host doesn't report per-field controls on the seam — soft-skip
143
+ if (field === undefined) return blockedDespiteAssertions('the instantiate seam reports no fields[] entry for the vendor field — the plain-text degrade is unobserved');
144
144
  expect(
145
145
  field.control !== undefined && PLAIN_TEXT_CONTROLS.has(field.control),
146
146
  req('openwop.it.form-content-instantiation.2-an-unrecognized-vendor-field-type-degrades-to-plain-text-and-does-not-fail-the',
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
- import { softSkip } from '../lib/soft-skip.js';
14
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
15
15
  import { readHeartbeatCap, heartbeatSupported, tickHeartbeat } from '../lib/heartbeat.js';
16
16
  import { req } from '../lib/requirement-ids.js';
17
17
 
@@ -35,7 +35,7 @@ describe('heartbeat-idempotent-no-spam (RFC 0060 §B.5)', () => {
35
35
  req('openwop.it.heartbeat-idempotent-no-spam.an-unchanged-tick-enqueues-nothing-only-a-transition-does', 'RFC 0060 §B.5', 'an unchanged tick MUST NOT emit heartbeat.stateChanged'),
36
36
  ).toBe(0);
37
37
  const transition = await tickHeartbeat({ heartbeatId: hb, observedState: { unread: 3 } });
38
- if (transition === null) return softSkip('blocked', 'precondition not met — `transition === null` returned early (seam, prior step, or fixture unavailable)');
38
+ if (transition === null) return blockedDespiteAssertions('the heartbeat seam answered the unchanged ticks but not the transition — the one-stateChanged-per-transition rule is unobserved');
39
39
  expect(
40
40
  changedCount(transition.json),
41
41
  req('openwop.it.heartbeat-idempotent-no-spam.an-unchanged-tick-enqueues-nothing-only-a-transition-does', 'RFC 0060 §B.5', 'a transitioning tick MUST emit exactly one heartbeat.stateChanged'),
@@ -83,7 +83,7 @@ describe.skipIf(SKIP)(
83
83
  expect(
84
84
  succeeded.length + conflicted.length,
85
85
  req('openwop.it.highConcurrency.10-parallel-requests-with-same-key-yield-one-runid-and-9-replays',
86
- 'idempotency.md §Concurrent duplicates',
86
+ 'spec/v1/idempotency.md §Concurrent duplicates',
87
87
  'every concurrent request MUST resolve to either the cached response or a deterministic 409',
88
88
  ),
89
89
  ).toBe(10);
@@ -92,7 +92,7 @@ describe.skipIf(SKIP)(
92
92
  expect(
93
93
  runIds.size,
94
94
  req('openwop.it.highConcurrency.10-parallel-requests-with-same-key-yield-one-runid-and-9-replays',
95
- 'idempotency.md §Layer 1',
95
+ 'spec/v1/idempotency.md §Layer 1',
96
96
  'same idempotency key MUST yield exactly ONE runId across all successful responses',
97
97
  ),
98
98
  ).toBe(1);
@@ -103,7 +103,7 @@ describe.skipIf(SKIP)(
103
103
  expect(
104
104
  c.errorCode === 'idempotency_in_flight' || c.errorCode === undefined,
105
105
  req('openwop.it.highConcurrency.10-parallel-requests-with-same-key-yield-one-runid-and-9-replays',
106
- 'idempotency.md',
106
+ 'spec/v1/idempotency.md',
107
107
  '409 on parallel idempotency-keyed retry MUST carry error="idempotency_in_flight"',
108
108
  ),
109
109
  ).toBe(true);
@@ -111,7 +111,7 @@ describe.skipIf(SKIP)(
111
111
  expect(
112
112
  c.retryAfter,
113
113
  req('openwop.it.highConcurrency.10-parallel-requests-with-same-key-yield-one-runid-and-9-replays',
114
- 'idempotency.md',
114
+ 'spec/v1/idempotency.md',
115
115
  '409 idempotency_in_flight MUST include a numeric retryAfter',
116
116
  ),
117
117
  ).toBeGreaterThan(0);
@@ -150,7 +150,7 @@ describe.skipIf(SKIP)(
150
150
  expect(
151
151
  uniqueRunIds.size,
152
152
  req('openwop.it.highConcurrency.10-parallel-requests-with-distinct-keys-yield-10-distinct-runids',
153
- 'idempotency.md §Layer 1',
153
+ 'spec/v1/idempotency.md §Layer 1',
154
154
  'distinct idempotency keys MUST yield distinct runIds (Layer 1 dedup is keyed)',
155
155
  ),
156
156
  ).toBe(succeededRunIds.length);
@@ -195,7 +195,7 @@ describe.skipIf(SKIP)(
195
195
  expect(
196
196
  runIds.size,
197
197
  req('openwop.it.highConcurrency.5-sequential-retries-with-same-key-100ms-apart-all-succeed-idempotency-cache-sur',
198
- 'idempotency.md §Layer 1',
198
+ 'spec/v1/idempotency.md §Layer 1',
199
199
  'all 5 retries with same key MUST resolve to the same runId',
200
200
  ),
201
201
  ).toBe(1);
@@ -204,7 +204,7 @@ describe.skipIf(SKIP)(
204
204
  expect(
205
205
  results[0]!.replay,
206
206
  req('openwop.it.highConcurrency.5-sequential-retries-with-same-key-100ms-apart-all-succeed-idempotency-cache-sur',
207
- 'idempotency.md §Server responsibilities',
207
+ 'spec/v1/idempotency.md §Server responsibilities',
208
208
  'first request with new key MUST NOT be marked as replay',
209
209
  ),
210
210
  ).toBe(false);
@@ -216,7 +216,7 @@ describe.skipIf(SKIP)(
216
216
  expect(
217
217
  someReplay,
218
218
  req('openwop.it.highConcurrency.5-sequential-retries-with-same-key-100ms-apart-all-succeed-idempotency-cache-sur',
219
- 'idempotency.md §Server responsibilities #2',
219
+ 'spec/v1/idempotency.md §Server responsibilities #2',
220
220
  'replay responses SHOULD set openwop-Idempotent-Replay: true',
221
221
  ),
222
222
  ).toBe(true);
@@ -252,7 +252,7 @@ describe.skipIf(SKIP)(
252
252
  expect(
253
253
  typeof ackTimeout === 'number' && Number.isInteger(ackTimeout) && ackTimeout >= 5,
254
254
  req('openwop.it.highConcurrency.concurrent-distinct-key-requests-respect-advertised-idempotency-cache-retention',
255
- 'idempotency.md',
255
+ 'spec/v1/idempotency.md',
256
256
  'limits.idempotencyAckTimeoutSec MUST be integer ≥5 when advertised',
257
257
  ),
258
258
  ).toBe(true);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" — the
2
+ * RFC 0150 §B / `spec/v1/idempotency.md` §"Concurrent duplicates (Layer 2)" — the
3
3
  * atomic-claim witness. Gap **G17**.
4
4
  *
5
5
  * The rule: the persist that guards a side effect MUST be an **atomic claim** —
@@ -102,7 +102,7 @@ describe('idempotency-concurrent-claim: two executors of one run, one effect (RF
102
102
  expect(
103
103
  unique.size,
104
104
  req('openwop.it.idempotency-concurrent-claim.every-executor-mints-the-same-logicalinvocationid-without-this-one-delivery-prov',
105
- 'idempotency.md §"Idempotency key composition" (Across a recovery boundary)',
105
+ 'spec/v1/idempotency.md §"Idempotency key composition" (Across a recovery boundary)',
106
106
  'all executors MUST mint one identity — differing ids mean they never collided, so a single delivery is a vacuous pass rather than a working claim',
107
107
  ),
108
108
  ).toBe(1);
@@ -119,7 +119,7 @@ describe('idempotency-concurrent-claim: two executors of one run, one effect (RF
119
119
  expect(
120
120
  out.delivered,
121
121
  req('openwop.it.idempotency-concurrent-claim.exactly-one-effect-escapes-however-many-executors-attempt-it',
122
- 'idempotency.md §"Concurrent duplicates (Layer 2)"',
122
+ 'spec/v1/idempotency.md §"Concurrent duplicates (Layer 2)"',
123
123
  'the engine MUST ensure at most one concurrent executor performs the external effect — the guarding persist MUST be an atomic claim, not a read-then-write',
124
124
  ),
125
125
  ).toBe(1);
@@ -156,7 +156,7 @@ describe('idempotency-concurrent-claim: two executors of one run, one effect (RF
156
156
  expect(
157
157
  out.delivered,
158
158
  req('openwop.it.idempotency-concurrent-claim.the-race-is-real-at-higher-concurrency-too-a-claim-that-only-holds-at-2-is-not-a',
159
- 'idempotency.md §"Concurrent duplicates (Layer 2)"',
159
+ 'spec/v1/idempotency.md §"Concurrent duplicates (Layer 2)"',
160
160
  'exactly one effect regardless of how many executors race — at most one wins the compare-and-set, the rest observe the hit',
161
161
  ),
162
162
  ).toBe(1);
@@ -167,7 +167,7 @@ describe('idempotency-concurrent-claim: two executors of one run, one effect (RF
167
167
  ).toBe(out.attempted);
168
168
  expect(
169
169
  new Set(ids.map((x) => JSON.stringify(x))).size,
170
- req('openwop.it.idempotency-concurrent-claim.the-race-is-real-at-higher-concurrency-too-a-claim-that-only-holds-at-2-is-not-a', 'idempotency.md §"Idempotency key composition"', 'one identity across all executors at any concurrency'),
170
+ req('openwop.it.idempotency-concurrent-claim.the-race-is-real-at-higher-concurrency-too-a-claim-that-only-holds-at-2-is-not-a', 'spec/v1/idempotency.md §"Idempotency key composition"', 'one identity across all executors at any concurrency'),
171
171
  ).toBe(1);
172
172
  });
173
173
  });
@@ -135,7 +135,7 @@ describe('category: core.openwop.http.idempotency-key — determinism contract',
135
135
  };
136
136
  const a = await idempotencyKey(ctx);
137
137
  const b = await idempotencyKey(ctx);
138
- expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.default-mode-composite-identical-runid-nodeid-payload-produces-identical-keys', 'idempotency.md §"Idempotency-Key', 'idempotency.md §Idempotency-Key: same logical request MUST produce same key')).toBe(b.outputs.key);
138
+ expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.default-mode-composite-identical-runid-nodeid-payload-produces-identical-keys', 'spec/v1/idempotency.md §"Idempotency-Key', 'spec/v1/idempotency.md §Idempotency-Key: same logical request MUST produce same key')).toBe(b.outputs.key);
139
139
  expect(a.outputs.key).toMatch(KEY_PATTERN);
140
140
  });
141
141
 
@@ -144,7 +144,7 @@ describe('category: core.openwop.http.idempotency-key — determinism contract',
144
144
  const baseCtx = { runId: 'run-1', nodeId: 'node-1' };
145
145
  const a = await idempotencyKey({ ...baseCtx, inputs: { payload: { hello: 'world' } } });
146
146
  const b = await idempotencyKey({ ...baseCtx, inputs: { payload: { hello: 'CHANGED' } } });
147
- expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.payload-sensitivity-different-payload-produces-different-key', 'idempotency.md §"Idempotency-Key', 'payload sensitivity — different payload produces different key')).not.toBe(b.outputs.key);
147
+ expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.payload-sensitivity-different-payload-produces-different-key', 'spec/v1/idempotency.md §"Idempotency-Key', 'payload sensitivity — different payload produces different key')).not.toBe(b.outputs.key);
148
148
  });
149
149
 
150
150
  it('run isolation — different runId produces different key', async () => {
@@ -152,7 +152,7 @@ describe('category: core.openwop.http.idempotency-key — determinism contract',
152
152
  const payload = { hello: 'world' };
153
153
  const a = await idempotencyKey({ runId: 'run-1', nodeId: 'node-1', inputs: { payload } });
154
154
  const b = await idempotencyKey({ runId: 'run-2', nodeId: 'node-1', inputs: { payload } });
155
- expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.run-isolation-different-runid-produces-different-key', 'idempotency.md §"Idempotency-Key', 'run isolation — different runId produces different key')).not.toBe(b.outputs.key);
155
+ expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.run-isolation-different-runid-produces-different-key', 'spec/v1/idempotency.md §"Idempotency-Key', 'run isolation — different runId produces different key')).not.toBe(b.outputs.key);
156
156
  });
157
157
 
158
158
  it('node isolation — different nodeId produces different key', async () => {
@@ -160,7 +160,7 @@ describe('category: core.openwop.http.idempotency-key — determinism contract',
160
160
  const payload = { hello: 'world' };
161
161
  const a = await idempotencyKey({ runId: 'run-1', nodeId: 'node-A', inputs: { payload } });
162
162
  const b = await idempotencyKey({ runId: 'run-1', nodeId: 'node-B', inputs: { payload } });
163
- expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.node-isolation-different-nodeid-produces-different-key', 'idempotency.md §"Idempotency-Key', 'node isolation — different nodeId produces different key')).not.toBe(b.outputs.key);
163
+ expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.node-isolation-different-nodeid-produces-different-key', 'spec/v1/idempotency.md §"Idempotency-Key', 'node isolation — different nodeId produces different key')).not.toBe(b.outputs.key);
164
164
  });
165
165
 
166
166
  it('hash mode is deterministic in the payload alone (run/node ignored)', async () => {
@@ -168,7 +168,7 @@ describe('category: core.openwop.http.idempotency-key — determinism contract',
168
168
  const payload = { request: 'payload-here', id: 42 };
169
169
  const a = await idempotencyKey({ config: { mode: 'hash' }, runId: 'run-1', nodeId: 'node-1', inputs: { payload } });
170
170
  const b = await idempotencyKey({ config: { mode: 'hash' }, runId: 'run-2', nodeId: 'node-2', inputs: { payload } });
171
- expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.hash-mode-is-deterministic-in-the-payload-alone-run-node-ignored', 'idempotency.md §"Idempotency-Key', 'hash mode MUST ignore run/node — useful for global remote-dedup caches')).toBe(b.outputs.key);
171
+ expect(a.outputs.key, req('openwop.it.idempotency-key-determinism.hash-mode-is-deterministic-in-the-payload-alone-run-node-ignored', 'spec/v1/idempotency.md §"Idempotency-Key', 'hash mode MUST ignore run/node — useful for global remote-dedup caches')).toBe(b.outputs.key);
172
172
  expect(a.outputs.key).toMatch(KEY_PATTERN);
173
173
  });
174
174
 
@@ -183,7 +183,7 @@ describe('category: core.openwop.http.idempotency-key — determinism contract',
183
183
  ];
184
184
  for (const ctx of samples) {
185
185
  const result = await idempotencyKey(ctx);
186
- expect(result.outputs.key, req('openwop.it.idempotency-key-determinism.output-shape-every-emitted-key-matches-openwop-sha256-prefix-16', 'idempotency.md §"Idempotency-Key', `key shape for ctx=${JSON.stringify(ctx)}`)).toMatch(KEY_PATTERN);
186
+ expect(result.outputs.key, req('openwop.it.idempotency-key-determinism.output-shape-every-emitted-key-matches-openwop-sha256-prefix-16', 'spec/v1/idempotency.md §"Idempotency-Key', `key shape for ctx=${JSON.stringify(ctx)}`)).toMatch(KEY_PATTERN);
187
187
  }
188
188
  });
189
189
 
@@ -197,7 +197,7 @@ describe('category: core.openwop.http.idempotency-key — determinism contract',
197
197
  } catch (err) {
198
198
  caught = err;
199
199
  }
200
- expect(caught, req('openwop.it.idempotency-key-determinism.uuid-mode-rejects-with-config-invalid-safety-fix-per-1-1-2', 'idempotency.md §"Idempotency-Key', 'mode: uuid MUST be rejected — the 1.1.0/1.1.1 non-deterministic default was removed')).toBeInstanceOf(Error);
200
+ expect(caught, req('openwop.it.idempotency-key-determinism.uuid-mode-rejects-with-config-invalid-safety-fix-per-1-1-2', 'spec/v1/idempotency.md §"Idempotency-Key', 'mode: uuid MUST be rejected — the 1.1.0/1.1.1 non-deterministic default was removed')).toBeInstanceOf(Error);
201
201
  expect((caught as Error & { code?: string }).code).toBe('CONFIG_INVALID');
202
202
  expect((caught as Error).message).toMatch(/uuid.*removed|safety-fix/i);
203
203
  });
@@ -223,7 +223,7 @@ describe('category: core.openwop.http.idempotency-key — determinism contract',
223
223
  const expected = canonicalCompositeKey(v.runId, v.nodeId, v.payload);
224
224
  expect(
225
225
  packed.outputs.key,
226
- req('openwop.it.idempotency-key-determinism.cross-impl-invariant-pack-output-equals-canonical-sha-256-formula', 'idempotency.md §"Idempotency-Key', `vector ${JSON.stringify(v)} — pack output MUST match canonical sha256(runId\\0nodeId\\0JSON(payload))`),
226
+ req('openwop.it.idempotency-key-determinism.cross-impl-invariant-pack-output-equals-canonical-sha-256-formula', 'spec/v1/idempotency.md §"Idempotency-Key', `vector ${JSON.stringify(v)} — pack output MUST match canonical sha256(runId\\0nodeId\\0JSON(payload))`),
227
227
  ).toBe(expected);
228
228
  }
229
229
 
@@ -47,14 +47,14 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + same body replays per
47
47
  expect(
48
48
  [200, 201].includes(replay.status),
49
49
  req('openwop.it.idempotency.returns-same-runid-twice-and-sets-openwop-idempotent-replay-on-the-replay',
50
- 'idempotency.md §Layer 1',
50
+ 'spec/v1/idempotency.md §Layer 1',
51
51
  'replay request with same key + same body MUST return success status (200/201)',
52
52
  ),
53
53
  ).toBe(true);
54
54
 
55
55
  const replayRunId = (replay.json as { runId: string }).runId;
56
56
  expect(replayRunId, req('openwop.it.idempotency.returns-same-runid-twice-and-sets-openwop-idempotent-replay-on-the-replay',
57
- 'idempotency.md §Layer 1',
57
+ 'spec/v1/idempotency.md §Layer 1',
58
58
  'replay MUST return the SAME runId (no new run created)',
59
59
  )).toBe(firstRunId);
60
60
 
@@ -84,7 +84,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + different body conflic
84
84
  );
85
85
 
86
86
  expect(conflict.status, req('openwop.it.idempotency.returns-409-when-the-body-changes-under-the-same-key',
87
- 'idempotency.md §Layer 1',
87
+ 'spec/v1/idempotency.md §Layer 1',
88
88
  'same Idempotency-Key with a different body MUST return 409',
89
89
  )).toBe(409);
90
90
 
@@ -98,7 +98,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + different body conflic
98
98
  // `idempotency_key_replay_mismatch` — a spelling in no corpus artifact at
99
99
  // all. This leg asserted the status alone, so every one of them passed.
100
100
  //
101
- // `idempotency_key_mismatch` is canonical (idempotency.md §"Record shape,
101
+ // `idempotency_key_mismatch` is canonical (spec/v1/idempotency.md §"Record shape,
102
102
  // digest, and lease"): the only spelling already in more than one shipped
103
103
  // artifact. The two legacy spellings are TOLERATED here through the first
104
104
  // minor after 2026-11-10 so a converging host is not red on a rename it is
@@ -109,7 +109,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + different body conflic
109
109
  expect(
110
110
  code === 'idempotency_key_mismatch' || LEGACY.includes(code ?? ''),
111
111
  req('openwop.it.idempotency.returns-409-when-the-body-changes-under-the-same-key',
112
- 'idempotency.md §"Record shape, digest, and lease"',
112
+ 'spec/v1/idempotency.md §"Record shape, digest, and lease"',
113
113
  `a different request digest under the same scoped key MUST fail with the canonical ` +
114
114
  `\`idempotency_key_mismatch\` (legacy \`${LEGACY.join('` / `')}\` tolerated through the ` +
115
115
  `first minor after 2026-11-10); got \`${code ?? '<none>'}\``,
@@ -122,7 +122,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + different body conflic
122
122
  expect(
123
123
  conflictRunId,
124
124
  req('openwop.it.idempotency.returns-409-when-the-body-changes-under-the-same-key',
125
- 'idempotency.md §"Record shape, digest, and lease"',
125
+ 'spec/v1/idempotency.md §"Record shape, digest, and lease"',
126
126
  'a digest mismatch MUST NOT return the cached body (no runId on the 409)',
127
127
  ),
128
128
  ).toBeUndefined();
@@ -6,7 +6,7 @@
6
6
  * additions documented in idempotency.md:
7
7
  *
8
8
  * 1. openwop-Idempotent-Replay header is present on every keyed response
9
- * (idempotency.md §Server responsibilities).
9
+ * (spec/v1/idempotency.md §Server responsibilities).
10
10
  * 2. Retry-budget floor — hosts handle ≥5 retries 100ms apart with
11
11
  * the cached response (scale-profiles.md §"Retry semantics").
12
12
  * 3. Same-key replay returns same runId across the budget.
@@ -53,7 +53,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency-retry: openwop-Idempotent-Replay h
53
53
  // Permissive assertion: if present, MUST be "false" or "true".
54
54
  if (firstReplay !== null) {
55
55
  expect(['false', 'true'].includes(firstReplay), req('openwop.it.idempotencyRetry.first-request-with-new-key-returns-false-or-absent-should-per-current-spec-repla',
56
- 'idempotency.md §Server responsibilities',
56
+ 'spec/v1/idempotency.md §Server responsibilities',
57
57
  'openwop-Idempotent-Replay value MUST be "true" or "false"',
58
58
  )).toBe(true);
59
59
  }
@@ -61,19 +61,19 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency-retry: openwop-Idempotent-Replay h
61
61
  const replay = await driver.post('/v1/runs', body, { headers: { 'Idempotency-Key': key } });
62
62
  expect(
63
63
  [200, 201].includes(replay.status),
64
- req('openwop.it.idempotencyRetry.first-request-with-new-key-returns-false-or-absent-should-per-current-spec-repla', 'idempotency.md §Layer 1', 'replay returns 200 or 201'),
64
+ req('openwop.it.idempotencyRetry.first-request-with-new-key-returns-false-or-absent-should-per-current-spec-repla', 'spec/v1/idempotency.md §Layer 1', 'replay returns 200 or 201'),
65
65
  ).toBe(true);
66
66
 
67
67
  const replayHeader = replay.headers.get('openwop-idempotent-replay');
68
- // Per idempotency.md §Server responsibilities #2: SHOULD be set.
68
+ // Per spec/v1/idempotency.md §Server responsibilities #2: SHOULD be set.
69
69
  // RFC 0002 §1 promotes to MUST. Today's strictness: present on replay.
70
70
  expect(replayHeader, req('openwop.it.idempotencyRetry.first-request-with-new-key-returns-false-or-absent-should-per-current-spec-repla',
71
- 'idempotency.md §Server responsibilities #2',
71
+ 'spec/v1/idempotency.md §Server responsibilities #2',
72
72
  'openwop-Idempotent-Replay SHOULD be set on idempotent replay responses',
73
73
  )).not.toBeNull();
74
74
  if (replayHeader !== null) {
75
75
  expect(replayHeader, req('openwop.it.idempotencyRetry.first-request-with-new-key-returns-false-or-absent-should-per-current-spec-repla',
76
- 'idempotency.md §Server responsibilities #2',
76
+ 'spec/v1/idempotency.md §Server responsibilities #2',
77
77
  'openwop-Idempotent-Replay on replay MUST be "true"',
78
78
  )).toBe('true');
79
79
  }
@@ -104,7 +104,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency-retry: 5-retry budget per scale-pr
104
104
 
105
105
  const runIds = new Set(responses.map((r) => (r.json as { runId?: string })?.runId));
106
106
  expect(runIds.size, req('openwop.it.idempotencyRetry.5-retries-100ms-apart-with-same-key-all-return-the-same-runid',
107
- 'idempotency.md §Layer 1',
107
+ 'spec/v1/idempotency.md §Layer 1',
108
108
  '5 retries with same key MUST collapse to exactly one runId',
109
109
  )).toBe(1);
110
110
  });
@@ -116,19 +116,24 @@ describe('idempotency-retry: limits.idempotencyAckTimeoutSec contract per idempo
116
116
  expect(res.status).toBe(200);
117
117
 
118
118
  const limits = (res.json as { limits?: Record<string, unknown> })?.limits;
119
- if (!limits) return softSkip('blocked', 'precondition not met — `!limits` returned early (limits required per capabilities.md §3 — covered elsewhere) (seam, prior step, or fixture unavailable)'); // limits required per capabilities.md §3 — covered elsewhere
119
+ // partial-witness-ok: discovery answered 200; `limits` presence is its own requirement,
120
+ // asserted by the capabilities scenarios, and this leg's IF-advertised clause binds nothing
121
+ // without it.
122
+ if (!limits) return softSkip('inapplicable', 'discovery carries no limits object — its presence is asserted elsewhere; nothing for this IF-advertised leg to check');
120
123
  const ack = limits.idempotencyAckTimeoutSec;
121
124
  if (ack === undefined) {
122
125
  // Per idempotency.md, the field is optional; absence implies the
123
126
  // 5-second floor. Nothing to assert.
124
- return softSkip('blocked', 'precondition not met — `ack === undefined` returned early (Per idempotency.md, the field is optional; absence implies the 5-second floor. Nothing to assert.) (seam, prior step, or fixture unavailable)');
127
+ // partial-witness-ok: idempotencyAckTimeoutSec is optional and absence means the 5-second
128
+ // floor; the IF-advertised MUSTs below bind nothing.
129
+ return softSkip('inapplicable', 'limits.idempotencyAckTimeoutSec not advertised (optional; the 5-second floor applies) — nothing to check');
125
130
  }
126
131
  expect(typeof ack === 'number' && Number.isInteger(ack), req('openwop.it.idempotencyRetry.host-advertising-idempotencyacktimeoutsec-sets-integer-5',
127
- 'idempotency.md',
132
+ 'spec/v1/idempotency.md',
128
133
  'limits.idempotencyAckTimeoutSec MUST be an integer when advertised',
129
134
  )).toBe(true);
130
135
  expect(ack as number, req('openwop.it.idempotencyRetry.host-advertising-idempotencyacktimeoutsec-sets-integer-5',
131
- 'idempotency.md',
136
+ 'spec/v1/idempotency.md',
132
137
  'limits.idempotencyAckTimeoutSec MUST be ≥ 5',
133
138
  )).toBeGreaterThanOrEqual(5);
134
139
  });
@@ -31,7 +31,7 @@ import { driver } from '../lib/driver.js';
31
31
  import { pollUntilStatus } from '../lib/polling.js';
32
32
  import { isFixtureAdvertised } from '../lib/fixtures.js';
33
33
  import { req } from '../lib/requirement-ids.js';
34
- import { softSkip } from '../lib/soft-skip.js';
34
+ import { blockedDespiteAssertions } from '../lib/soft-skip.js';
35
35
 
36
36
  const FIXTURE = 'conformance-interrupt-external-event';
37
37
  const SKIP = !isFixtureAdvertised(FIXTURE);
@@ -72,26 +72,38 @@ describe.skipIf(SKIP)('interrupt-token-matrix: POST /v1/interrupts/{token} negat
72
72
  expect(create.status).toBe(201);
73
73
  const runId = (create.json as { runId: string }).runId;
74
74
 
75
- await pollUntilStatus(runId, 'waiting-external-event', { timeoutMs: 10_000 }).catch(() => null);
75
+ // softskip ratchet, 2026-09-27: this leg polled for `waiting-external-event`, a
76
+ // status that does not exist, and read `snapshot.interrupts[].token`, a field the
77
+ // snapshot does not carry. It found no token on any host, so it never reached the
78
+ // replay assertion and recorded a partial-witness pass on the 201 alone. It now
79
+ // reads the token the way interrupt-external-event-correlation does
80
+ // (`interrupt.interruptToken`, else the `/v1/interrupts/{token}` segment of
81
+ // `interrupt.callbackUrl`) once the run reaches `waiting-external`.
82
+ await pollUntilStatus(runId, 'waiting-external', { timeoutMs: 10_000 }).catch(() => null);
76
83
 
77
84
  const snap = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
78
- const interrupts =
79
- ((snap.json as { interrupts?: Array<{ token?: string }> }).interrupts ?? [])
80
- .filter((i) => typeof i.token === 'string');
81
- const token = interrupts[0]?.token;
85
+ const interrupt = (snap.json as { interrupt?: { interruptToken?: string; callbackUrl?: string } } | undefined)?.interrupt;
86
+ const fromCallback = interrupt?.callbackUrl?.match(/\/v1\/interrupts\/([^/?]+)/)?.[1];
87
+ const token = interrupt?.interruptToken ?? (fromCallback === undefined ? undefined : decodeURIComponent(fromCallback));
82
88
  if (typeof token !== 'string') {
83
89
  // eslint-disable-next-line no-console
84
90
  console.warn('[interrupt-token-matrix] host did not surface an interrupt token; skipping replay subtest');
85
91
  await driver.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {
86
92
  reason: 'conformance-cleanup',
87
93
  });
88
- return softSkip('blocked', 'precondition not met — `typeof token !== \'string\'` returned early ([interrupt-token-matrix] host did not surface an interrupt token; skipping replay subtest) (seam, prior step, or fixture unavailable)');
94
+ // interrupt-external-event-correlation holds token exposure as a MUST for this
95
+ // fixture, so a missing token leaves the replay rule unobserved: blocked.
96
+ return blockedDespiteAssertions('the suspended conformance-interrupt-external-event run exposed no signed token (interrupt.interruptToken or interrupt.callbackUrl) — replay of a resolved token is unobserved');
89
97
  }
90
98
 
99
+ // The fixture's correlation is {orderId: 'fixture-order-1', status: 'completed'}; a
100
+ // mismatched payload is refused 422 (interrupt-external-event-correlation), so the
101
+ // first resolve must match or the replay rule is never reached.
102
+ const MATCHING = { orderId: 'fixture-order-1', status: 'completed', externalReference: 'conformance-token-matrix' };
91
103
  const resolve1 = await driver.post(`/v1/interrupts/${encodeURIComponent(token)}`, {
92
- correlation: { orderId: 'order-token-matrix', status: 'accepted' },
104
+ resumeValue: MATCHING,
93
105
  });
94
- if (resolve1.status !== 200 && resolve1.status !== 202) {
106
+ if (resolve1.status < 200 || resolve1.status >= 300) {
95
107
  // eslint-disable-next-line no-console
96
108
  console.warn(
97
109
  `[interrupt-token-matrix] first resolve returned ${resolve1.status}; can't exercise replay path. Skipping.`,
@@ -99,11 +111,12 @@ describe.skipIf(SKIP)('interrupt-token-matrix: POST /v1/interrupts/{token} negat
99
111
  await driver.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {
100
112
  reason: 'conformance-cleanup',
101
113
  });
102
- return softSkip('blocked', 'precondition not met — `resolve1.status !== 200 && resolve1.status !== 202` returned early ([interrupt-token-matrix] first resolve returned …; can\'t exercise replay path. Skipping.) (seam, prior step, or fixture unava…');
114
+ // interrupt-external-event-correlation holds a matching resolve to 2xx as a MUST.
115
+ return blockedDespiteAssertions(`the first resolve of the token answered ${resolve1.status}, not 2xx — replay of a resolved token is unobserved`);
103
116
  }
104
117
 
105
118
  const resolve2 = await driver.post(`/v1/interrupts/${encodeURIComponent(token)}`, {
106
- correlation: { orderId: 'order-token-matrix', status: 'accepted' },
119
+ resumeValue: MATCHING,
107
120
  });
108
121
  expect([404, 409, 410], req('openwop.it.interrupt-token-matrix.replay-after-successful-resolve-returns-409-or-404',
109
122
  'rest-endpoints.md POST /v1/interrupts/{token}',
@@ -22,7 +22,7 @@
22
22
 
23
23
  import { describe, it, expect } from 'vitest';
24
24
  import { driver } from '../lib/driver.js';
25
- import { seamAbsent, softSkip } from '../lib/soft-skip.js';
25
+ import { seamAbsent, blockedDespiteAssertions } from '../lib/soft-skip.js';
26
26
  import { mcpServerMount } from '../lib/mcp-mount.js';
27
27
  import { behaviorGate } from '../lib/behavior-gate.js';
28
28
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
@@ -56,10 +56,10 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §D — mcp-cache-tenan
56
56
  // Shape only; the cross-caller half is unobservable without a second credential.
57
57
  // eslint-disable-next-line no-console
58
58
  console.warn('[mcp-cache-tenant-scope] OPENWOP_TEST_SECONDARY_API_KEY not set — cross-caller half not exercised (blocked)');
59
- return softSkip('blocked', 'precondition not met — `!other` returned early ([mcp-cache-tenant-scope] OPENWOP_TEST_SECONDARY_API_KEY not set — cross-caller half not exercised (blocked)) (seam, prior step, or fixture unavailable)');
59
+ return blockedDespiteAssertions('OPENWOP_TEST_SECONDARY_API_KEY is unset — the cross-caller half (a per-caller list MUST be cacheScope private) is unobserved');
60
60
  }
61
61
  const theirs = await listAs(other);
62
- if (theirs.status !== 200) return softSkip('blocked', 'precondition not met — `theirs.status !== 200` returned early (seam, prior step, or fixture unavailable)');
62
+ if (theirs.status !== 200) return blockedDespiteAssertions('tools/list under the secondary credential did not answer 200 — the cross-caller comparison is unobserved');
63
63
  const same = JSON.stringify(mine.body.result?.tools) === JSON.stringify(theirs.body.result?.tools);
64
64
  if (!same) {
65
65
  expect(mine.body.result?.cacheScope, req('openwop.it.mcp-cache-tenant-scope.a-per-caller-list-is-cachescope-private-a-public-list-is-byte-identical-across-c', 'mcp-integration.md §D', 'a list that differs per caller MUST be cacheScope private — a public list that differs is cross-context cache poisoning (mcp-cache-tenant-scoped)')).toBe('private');