@openwop/openwop-conformance 2.44.9 → 2.45.1

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 (66) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -2
  3. package/dist/spec-artifacts.lock.json +2 -2
  4. package/package.json +2 -2
  5. package/requirements.json +150 -131
  6. package/scenario-majors.json +5 -2
  7. package/schemas/CORPUS-STAMP.json +12 -12
  8. package/src/lib/run-list-walk.ts +65 -0
  9. package/src/scenarios/agentConfidenceEscalation.test.ts +8 -1
  10. package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +8 -1
  11. package/src/scenarios/agentMemoryRedactionContract.test.ts +8 -1
  12. package/src/scenarios/agentMemoryRoundTrip.test.ts +8 -1
  13. package/src/scenarios/agentMemoryTtlExpiry.test.ts +8 -1
  14. package/src/scenarios/agentMessageReducer.test.ts +8 -1
  15. package/src/scenarios/agentMetadata.test.ts +8 -1
  16. package/src/scenarios/agentPackCatalog.test.ts +13 -5
  17. package/src/scenarios/agentPackExport.test.ts +8 -2
  18. package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +17 -2
  19. package/src/scenarios/agentPackInstall.test.ts +8 -2
  20. package/src/scenarios/agentPackProvenance.test.ts +8 -1
  21. package/src/scenarios/agentReasoningEvents.test.ts +9 -1
  22. package/src/scenarios/agentReasoningStreaming.test.ts +14 -1
  23. package/src/scenarios/append-ordering.test.ts +7 -2
  24. package/src/scenarios/bulk-cancel.test.ts +11 -1
  25. package/src/scenarios/cap-breach.test.ts +8 -1
  26. package/src/scenarios/channel-ttl.test.ts +7 -1
  27. package/src/scenarios/conversationCapabilityNegotiation.test.ts +8 -1
  28. package/src/scenarios/conversationLifecycle.test.ts +8 -1
  29. package/src/scenarios/conversationReplayDeterminism.test.ts +8 -2
  30. package/src/scenarios/conversationVsLegacySuspend.test.ts +8 -1
  31. package/src/scenarios/cost-attribution.test.ts +17 -4
  32. package/src/scenarios/debugBundle.test.ts +16 -5
  33. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +10 -2
  34. package/src/scenarios/dispatch-output-mapping.test.ts +20 -4
  35. package/src/scenarios/dispatchLoop.test.ts +7 -1
  36. package/src/scenarios/durability-poison-exhaustion.test.ts +7 -2
  37. package/src/scenarios/highConcurrency.test.ts +11 -1
  38. package/src/scenarios/host-callback-declaration.test.ts +10 -1
  39. package/src/scenarios/identity-passthrough.test.ts +7 -1
  40. package/src/scenarios/interrupt-approval.test.ts +25 -5
  41. package/src/scenarios/interrupt-auth-required-resume.test.ts +9 -3
  42. package/src/scenarios/interrupt-external-event-correlation.test.ts +9 -2
  43. package/src/scenarios/interrupt-parent-child-cascade.test.ts +10 -2
  44. package/src/scenarios/interrupt-quorum-resolution.test.ts +9 -2
  45. package/src/scenarios/interrupt-token-matrix.test.ts +11 -3
  46. package/src/scenarios/interruptRace.test.ts +13 -3
  47. package/src/scenarios/multi-agent-confidence-escalation.test.ts +14 -3
  48. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +15 -3
  49. package/src/scenarios/multi-node-ordering.test.ts +7 -1
  50. package/src/scenarios/node-failed-payload-shape.test.ts +7 -1
  51. package/src/scenarios/orchestratorConservativePath.test.ts +8 -1
  52. package/src/scenarios/orchestratorDispatch.test.ts +9 -1
  53. package/src/scenarios/orchestratorTermination.test.ts +8 -1
  54. package/src/scenarios/owner-subject-echo.test.ts +9 -2
  55. package/src/scenarios/pause-resume.test.ts +19 -8
  56. package/src/scenarios/prompt-all-four-kinds-events.test.ts +9 -2
  57. package/src/scenarios/prompt-end-to-end-events.test.ts +10 -2
  58. package/src/scenarios/replay-fork-arbitrary.test.ts +11 -4
  59. package/src/scenarios/restart-during-run.test.ts +7 -1
  60. package/src/scenarios/secret-leakage-otel-attribute.test.ts +14 -5
  61. package/src/scenarios/stream-text-fixture.test.ts +8 -2
  62. package/src/scenarios/streamReconnect.test.ts +8 -2
  63. package/src/scenarios/subworkflow.test.ts +9 -1
  64. package/src/scenarios/v2-cors-preflight.test.ts +141 -0
  65. package/src/scenarios/v2-run-list.test.ts +14 -16
  66. package/src/scenarios/version-fold.test.ts +8 -2
@@ -37,7 +37,7 @@ import { capabilityFamily } from '../lib/discovery-capabilities.js';
37
37
  import { pollUntilStatus, pollUntilTerminal } from '../lib/polling.js';
38
38
  import { isFixtureAdvertised } from '../lib/fixtures.js';
39
39
  import { req } from '../lib/requirement-ids.js';
40
- import { softSkip } from '../lib/soft-skip.js';
40
+ import { softSkip, type SoftSkipKind } from '../lib/soft-skip.js';
41
41
 
42
42
  const FIXTURE =
43
43
  (isFixtureAdvertised('conformance-cancellable') && 'conformance-cancellable') ||
@@ -77,8 +77,13 @@ async function eventTypes(runId: string): Promise<string[]> {
77
77
  return Array.isArray(events) ? events.map((e) => String((e as { type?: unknown }).type)) : [];
78
78
  }
79
79
 
80
- describe.skipIf(SKIP)('pause/resume: running → paused → running → terminal', () => {
80
+ /** Why the gate below holds, as RFC 0148 §A names it (openwop#1686: a describe-level skip recorded no disposition). */
81
+ const GATE_WHY: readonly [SoftSkipKind, string] =
82
+ (!FIXTURE) ? ['blocked', `no candidate fixture for this scenario is advertised`] as const : ['blocked', 'the gate held for no named reason'] as const;
83
+
84
+ describe('pause/resume: running → paused → running → terminal', () => {
81
85
  it('pause transitions to paused; resume returns the run to running', async () => {
86
+ if (SKIP) return softSkip(...GATE_WHY);
82
87
  const create = await driver.post('/v1/runs', {
83
88
  workflowId: FIXTURE!,
84
89
  // 3 s, not 30 s: the resumed node.started is a FRESH attempt (runs.md
@@ -125,8 +130,9 @@ describe.skipIf(SKIP)('pause/resume: running → paused → running → terminal
125
130
  });
126
131
  });
127
132
 
128
- describe.skipIf(SKIP)('pause/resume: :resume on a non-paused run returns 409', () => {
133
+ describe('pause/resume: :resume on a non-paused run returns 409', () => {
129
134
  it('resuming a running (not paused) run returns 409 with details.runStatus', async () => {
135
+ if (SKIP) return softSkip(...GATE_WHY);
130
136
  const create = await driver.post('/v1/runs', {
131
137
  workflowId: FIXTURE!,
132
138
  inputs: { delayMs: 30_000 },
@@ -159,8 +165,9 @@ describe.skipIf(SKIP)('pause/resume: :resume on a non-paused run returns 409', (
159
165
  });
160
166
  });
161
167
 
162
- describe.skipIf(SKIP)('pause/resume: a second :pause is 409 without a matching Idempotency-Key and 202 with one', () => {
168
+ describe('pause/resume: a second :pause is 409 without a matching Idempotency-Key and 202 with one', () => {
163
169
  it(':pause on an already-paused run returns 409 with details.runStatus paused, unless the request carries the original Idempotency-Key', async () => {
170
+ if (SKIP) return softSkip(...GATE_WHY);
164
171
  const create = await driver.post('/v1/runs', {
165
172
  workflowId: FIXTURE!,
166
173
  inputs: { delayMs: 30_000 },
@@ -220,8 +227,9 @@ describe.skipIf(SKIP)('pause/resume: a second :pause is 409 without a matching I
220
227
  });
221
228
  });
222
229
 
223
- describe.skipIf(SKIP)('pause/resume: :pause on a terminal run returns 409', () => {
230
+ describe('pause/resume: :pause on a terminal run returns 409', () => {
224
231
  it(':pause on a completed/cancelled/failed run MUST return 409', async () => {
232
+ if (SKIP) return softSkip(...GATE_WHY);
225
233
  const create = await driver.post('/v1/runs', {
226
234
  workflowId: 'conformance-noop',
227
235
  });
@@ -244,8 +252,9 @@ describe.skipIf(SKIP)('pause/resume: :pause on a terminal run returns 409', () =
244
252
  });
245
253
  });
246
254
 
247
- describe.skipIf(SKIP)('pause/resume: drain-current-node lets the executing node reach a terminal first', () => {
255
+ describe('pause/resume: drain-current-node lets the executing node reach a terminal first', () => {
248
256
  it('under drain-current-node node.completed precedes run.paused in the log', async () => {
257
+ if (SKIP) return softSkip(...GATE_WHY);
249
258
  // A short node: drain has something to wait for that finishes inside the
250
259
  // poll window. A 30 s node under drain pauses after 30 s — that is the
251
260
  // semantic, not a bug (rest-endpoints.md: "lets the executing node reach
@@ -283,8 +292,9 @@ describe.skipIf(SKIP)('pause/resume: drain-current-node lets the executing node
283
292
  });
284
293
  });
285
294
 
286
- describe.skipIf(SKIP)('pause/resume: :pause-during-suspend race', () => {
295
+ describe('pause/resume: :pause-during-suspend race', () => {
287
296
  it(':pause MUST NOT silently override an active interrupt suspend', async () => {
297
+ if (SKIP) return softSkip(...GATE_WHY);
288
298
  // If the host seeds an approval fixture, drive a suspend then attempt
289
299
  // :pause. The expected behavior is that :pause either (a) noops with
290
300
  // 409 because the run is already waiting-approval (not in a pausable
@@ -339,8 +349,9 @@ describe.skipIf(SKIP)('pause/resume: :pause-during-suspend race', () => {
339
349
  // `capabilities.md` §`runs.pauseResume`. When a host advertises
340
350
  // `drainPolicies[]`, each advertised value MUST be accepted with 202.
341
351
  // Skips entirely when no advertisement is present.
342
- describe.skipIf(SKIP)('pause/resume: drainPolicy discrimination per capabilities advertisement', () => {
352
+ describe('pause/resume: drainPolicy discrimination per capabilities advertisement', () => {
343
353
  it('every drainPolicy advertised by the host is accepted on :pause', async () => {
354
+ if (SKIP) return softSkip(...GATE_WHY);
344
355
  const disco = await driver.get('/.well-known/openwop');
345
356
  const drainPolicies =
346
357
  capabilityFamily<{ pauseResume?: { drainPolicies?: string[] } }>(disco.json, 'runs')
@@ -36,7 +36,7 @@ import { isFixtureAdvertised } from '../lib/fixtures.js';
36
36
  import { behaviorGate } from '../lib/behavior-gate.js';
37
37
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
38
38
  import { req } from '../lib/requirement-ids.js';
39
- import { softSkip } from '../lib/soft-skip.js';
39
+ import { softSkip, type SoftSkipKind } from '../lib/soft-skip.js';
40
40
 
41
41
  const WORKFLOW_ID = 'conformance-prompt-all-four-kinds';
42
42
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
@@ -79,8 +79,14 @@ async function readAllEvents(runId: string): Promise<RunEventDoc[]> {
79
79
 
80
80
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
81
81
 
82
- describe.skipIf(SKIP_NO_FIXTURE || HTTP_SKIP)('prompt-all-four-kinds-events: each PromptKind dispatches end-to-end (RFC 0027 §A)', () => {
82
+ /** Why the gate below holds, as RFC 0148 §A names it (openwop#1686: a describe-level skip recorded no disposition). */
83
+ const GATE_WHY: readonly [SoftSkipKind, string] =
84
+ (!process.env.OPENWOP_BASE_URL) ? ['inapplicable', `no target: OPENWOP_BASE_URL is unset`] as const :
85
+ (!isFixtureAdvertised(WORKFLOW_ID)) ? ['blocked', `the \`${WORKFLOW_ID}\` fixture is not advertised`] as const : ['blocked', 'the gate held for no named reason'] as const;
86
+
87
+ describe('prompt-all-four-kinds-events: each PromptKind dispatches end-to-end (RFC 0027 §A)', () => {
83
88
  it('emits agent.promptResolved + prompt.composed for system, user, schema-hint, and few-shot kinds', async () => {
89
+ if (SKIP_NO_FIXTURE || HTTP_SKIP) return softSkip(...GATE_WHY);
84
90
  const d = await readDiscovery();
85
91
  if (!behaviorGate('prompts-supported', promptsSupported(d))) return;
86
92
 
@@ -171,6 +177,7 @@ describe.skipIf(SKIP_NO_FIXTURE || HTTP_SKIP)('prompt-all-four-kinds-events: eac
171
177
  });
172
178
 
173
179
  it('emits the first agent.promptResolved before the first prompt.composed (resolution-precedes-composition ordering)', async () => {
180
+ if (SKIP_NO_FIXTURE || HTTP_SKIP) return softSkip(...GATE_WHY);
174
181
  const d = await readDiscovery();
175
182
  if (!behaviorGate('prompts-supported', promptsSupported(d))) return;
176
183
  const create = await driver.post('/v1/runs', { workflowId: WORKFLOW_ID });
@@ -36,7 +36,7 @@ import { isFixtureAdvertised } from '../lib/fixtures.js';
36
36
  import { behaviorGate } from '../lib/behavior-gate.js';
37
37
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
38
38
  import { req } from '../lib/requirement-ids.js';
39
- import { softSkip } from '../lib/soft-skip.js';
39
+ import { softSkip, type SoftSkipKind } from '../lib/soft-skip.js';
40
40
 
41
41
  const WORKFLOW_ID = 'conformance-prompt-end-to-end';
42
42
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
@@ -82,8 +82,14 @@ async function readAllEvents(runId: string): Promise<RunEventDoc[]> {
82
82
 
83
83
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
84
84
 
85
- describe.skipIf(SKIP_NO_FIXTURE || HTTP_SKIP)('prompt-end-to-end-events: real dispatch emits agent.promptResolved + prompt.composed (RFC 0027/0029)', () => {
85
+ /** Why the gate below holds, as RFC 0148 §A names it (openwop#1686: a describe-level skip recorded no disposition). */
86
+ const GATE_WHY: readonly [SoftSkipKind, string] =
87
+ (!process.env.OPENWOP_BASE_URL) ? ['inapplicable', `no target: OPENWOP_BASE_URL is unset`] as const :
88
+ (!isFixtureAdvertised(WORKFLOW_ID)) ? ['blocked', `the \`${WORKFLOW_ID}\` fixture is not advertised`] as const : ['blocked', 'the gate held for no named reason'] as const;
89
+
90
+ describe('prompt-end-to-end-events: real dispatch emits agent.promptResolved + prompt.composed (RFC 0027/0029)', () => {
86
91
  it('emits agent.promptResolved with chain[].applied: true for layer "node" when systemPromptRef is set on node.config', async () => {
92
+ if (SKIP_NO_FIXTURE || HTTP_SKIP) return softSkip(...GATE_WHY);
87
93
  const d = await readDiscovery();
88
94
  if (!behaviorGate('prompts-supported', promptsSupported(d))) return;
89
95
 
@@ -136,6 +142,7 @@ describe.skipIf(SKIP_NO_FIXTURE || HTTP_SKIP)('prompt-end-to-end-events: real di
136
142
  });
137
143
 
138
144
  it('emits prompt.composed with sha256:<hex64> hash + non-empty composed body for system-kind template', async () => {
145
+ if (SKIP_NO_FIXTURE || HTTP_SKIP) return softSkip(...GATE_WHY);
139
146
  const d = await readDiscovery();
140
147
  if (!behaviorGate('prompts-supported', promptsSupported(d))) return;
141
148
 
@@ -182,6 +189,7 @@ describe.skipIf(SKIP_NO_FIXTURE || HTTP_SKIP)('prompt-end-to-end-events: real di
182
189
  });
183
190
 
184
191
  it('emits agent.promptResolved before prompt.composed (causal ordering)', async () => {
192
+ if (SKIP_NO_FIXTURE || HTTP_SKIP) return softSkip(...GATE_WHY);
185
193
  const d = await readDiscovery();
186
194
  if (!behaviorGate('prompts-supported', promptsSupported(d))) return;
187
195
  const create = await driver.post('/v1/runs', { workflowId: WORKFLOW_ID });
@@ -30,7 +30,7 @@
30
30
 
31
31
  import { describe, it, expect } from 'vitest';
32
32
  import { driver } from '../lib/driver.js';
33
- import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
33
+ import { softSkip, blockedDespiteAssertions, type SoftSkipKind } from '../lib/soft-skip.js';
34
34
  import { forkDeclined } from '../lib/fork-availability.js';
35
35
  import { pollUntilTerminal } from '../lib/polling.js';
36
36
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -119,10 +119,15 @@ function structuralShape(
119
119
  }));
120
120
  }
121
121
 
122
- describe.skipIf(SKIP_NO_MULTI)(
122
+ /** Why the gate below holds, as RFC 0148 §A names it (openwop#1686: a describe-level skip recorded no disposition). */
123
+ const GATE_WHY: readonly [SoftSkipKind, string] =
124
+ (!isFixtureAdvertised(MULTI_NODE_WORKFLOW_ID)) ? ['blocked', `the \`${MULTI_NODE_WORKFLOW_ID}\` fixture is not advertised`] as const : ['blocked', 'the gate held for no named reason'] as const;
125
+
126
+ describe(
123
127
  'replay-fork-arbitrary: fork from mid-range fromSeq in replay mode reaches terminal',
124
128
  () => {
125
129
  it('mid-fromSeq replay fork produces a new run that reaches `completed`', async (ctx) => {
130
+ if (SKIP_NO_MULTI) return softSkip(...GATE_WHY);
126
131
  const replay = await fetchReplayCapability();
127
132
  if (replay?.supported !== true) {
128
133
  softSkip('inapplicable', "host does not advertise `replay.supported: true` — the replay contract does not apply to it");
@@ -197,12 +202,13 @@ describe.skipIf(SKIP_NO_MULTI)(
197
202
  },
198
203
  );
199
204
 
200
- describe.skipIf(SKIP_NO_MULTI)(
205
+ describe(
201
206
  'replay-fork-arbitrary: two replay forks at the same mid-range fromSeq yield identical event shape',
202
207
  () => {
203
208
  it(
204
209
  'mid-fromSeq determinism — same source + same fromSeq → identical post-fork events (modulo IDs + timestamps)',
205
210
  async (ctx) => {
211
+ if (SKIP_NO_MULTI) return softSkip(...GATE_WHY);
206
212
  const replay = await fetchReplayCapability();
207
213
  if (replay?.supported !== true) {
208
214
  ctx.skip();
@@ -294,10 +300,11 @@ describe.skipIf(SKIP_NO_MULTI)(
294
300
  },
295
301
  );
296
302
 
297
- describe.skipIf(SKIP_NO_MULTI)(
303
+ describe(
298
304
  'replay-fork-arbitrary: fork from mid-range fromSeq in branch mode reaches terminal with overlay',
299
305
  () => {
300
306
  it('mid-fromSeq branch fork with empty overlay produces a new run that reaches `completed`', async (ctx) => {
307
+ if (SKIP_NO_MULTI) return softSkip(...GATE_WHY);
301
308
  const replay = await fetchReplayCapability();
302
309
  if (replay?.supported !== true) {
303
310
  softSkip('inapplicable', "host does not advertise `replay.supported: true` — the replay contract does not apply to it");
@@ -37,6 +37,7 @@ import { tmpdir } from 'node:os';
37
37
  import { join } from 'node:path';
38
38
  import { spawnHost, type SpawnedHost } from '../lib/multiProcess.js';
39
39
  import { req } from '../lib/requirement-ids.js';
40
+ import { softSkip, type SoftSkipKind } from '../lib/soft-skip.js';
40
41
 
41
42
  const HOST_PACKAGE_DIR =
42
43
  process.env.OPENWOP_RESTART_DURING_RUN_HOST_DIR ?? 'examples/hosts/sqlite';
@@ -78,12 +79,17 @@ afterEach(async () => {
78
79
  }
79
80
  });
80
81
 
81
- describe.skipIf(!RUN_THIS_SCENARIO)(
82
+ /** Why the gate below holds, as RFC 0148 §A names it (openwop#1686: a describe-level skip recorded no disposition). */
83
+ const GATE_WHY: readonly [SoftSkipKind, string] =
84
+ (!RUN_THIS_SCENARIO) ? ['skipped', `the operator did not opt in (OPENWOP_RUN_RESTART_DURING_RUN=1)`] as const : ['blocked', 'the gate held for no named reason'] as const;
85
+
86
+ describe(
82
87
  'restart-during-run: SIGKILL + same-port restart resumes orphaned run',
83
88
  () => {
84
89
  it(
85
90
  'mid-run SIGKILL on the host process; restart at the same port + DB resumes to terminal',
86
91
  async () => {
92
+ if (!RUN_THIS_SCENARIO) return softSkip(...GATE_WHY);
87
93
  workdir = mkdtempSync(join(tmpdir(), 'openwop-restart-during-run-'));
88
94
  const dbPath = join(workdir, 'openwop-host.sqlite');
89
95
 
@@ -58,7 +58,7 @@ import { isFixtureAdvertised } from '../lib/fixtures.js';
58
58
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
59
59
  import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
60
60
  import { req } from '../lib/requirement-ids.js';
61
- import { softSkip } from '../lib/soft-skip.js';
61
+ import { softSkip, type SoftSkipKind } from '../lib/soft-skip.js';
62
62
 
63
63
  /**
64
64
  * Callback-shaped: the host exports OTLP spans to the suite's collector, which scans them for the BYOK canary.
@@ -102,10 +102,16 @@ async function startByokRun(): Promise<string | null> {
102
102
  return (create.json as { runId: string }).runId;
103
103
  }
104
104
 
105
- describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
105
+ /** Why the gate below holds, as RFC 0148 §A names it (openwop#1686: a describe-level skip recorded no disposition). */
106
+ const GATE_WHY: readonly [SoftSkipKind, string] =
107
+ (!process.env.OPENWOP_BASE_URL) ? ['inapplicable', `no target: OPENWOP_BASE_URL is unset`] as const :
108
+ (!isFixtureAdvertised(BYOK_WORKFLOW_ID)) ? ['blocked', `the \`${BYOK_WORKFLOW_ID}\` fixture is not advertised`] as const : ['blocked', 'the gate held for no named reason'] as const;
109
+
110
+ describe(
106
111
  'secret-leakage-otel-attribute: OTel span scrape (RFC 0034 §B)',
107
112
  () => {
108
113
  it('NO OTel span attribute MUST contain the BYOK canary plaintext for a run that resolved it', async (ctx) => {
114
+ if (HTTP_SKIP || FIXTURE_SKIP) return softSkip(...GATE_WHY);
109
115
  if (!CANARY_VALUE) {
110
116
  ctx.skip();
111
117
  return softSkip('blocked', 'precondition not met — `!CANARY_VALUE` returned early (seam, prior step, or fixture unavailable)');
@@ -171,10 +177,11 @@ describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
171
177
  },
172
178
  );
173
179
 
174
- describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
180
+ describe(
175
181
  'secret-leakage-debug-bundle-otel: debug-bundle export scrape (RFC 0034 §B)',
176
182
  () => {
177
183
  it('NO debug-bundle field MUST contain the BYOK canary plaintext for a run that resolved it', async (ctx) => {
184
+ if (HTTP_SKIP || FIXTURE_SKIP) return softSkip(...GATE_WHY);
178
185
  if (!CANARY_VALUE) {
179
186
  ctx.skip();
180
187
  return softSkip('blocked', 'precondition not met — `!CANARY_VALUE` returned early (seam, prior step, or fixture unavailable)');
@@ -216,7 +223,7 @@ describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
216
223
  },
217
224
  );
218
225
 
219
- describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
226
+ describe(
220
227
  'secret-leakage-otel-attribute: real OTLP export scrape (collector-side)',
221
228
  () => {
222
229
  // Distinct from the scrape-seam probe above: this asserts against what
@@ -228,6 +235,7 @@ describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
228
235
  // attributes" gap. Gated on the in-process collector being active
229
236
  // (`OPENWOP_OTEL_COLLECTOR=true` + the host configured to export to it).
230
237
  it('NO real-exported OTel span/metric attribute MUST contain the BYOK canary plaintext', async (ctx) => {
238
+ if (HTTP_SKIP || FIXTURE_SKIP) return softSkip(...GATE_WHY);
231
239
  const collector = getCollector();
232
240
  if (!collector || !CANARY_VALUE) {
233
241
  ctx.skip();
@@ -267,10 +275,11 @@ describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
267
275
  },
268
276
  );
269
277
 
270
- describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
278
+ describe(
271
279
  'secret-leakage-otel-attribute: advertisement-shape probe (RFC 0034 §A)',
272
280
  () => {
273
281
  it('when secrets.supported is true, observability.testSeams advertisements MUST be boolean if present', async (ctx) => {
282
+ if (HTTP_SKIP || FIXTURE_SKIP) return softSkip(...GATE_WHY);
274
283
  const d = await readDiscovery();
275
284
  if (capabilityFamily<{ supported?: unknown }>(d, 'secrets')?.supported !== true) {
276
285
  ctx.skip();
@@ -32,7 +32,7 @@
32
32
  */
33
33
 
34
34
  import { describe, it, expect } from 'vitest';
35
- import { softSkip } from '../lib/soft-skip.js';
35
+ import { softSkip, type SoftSkipKind } from '../lib/soft-skip.js';
36
36
  import { driver, type OpenWOPResponse } from '../lib/driver.js';
37
37
  import { subscribe } from '../lib/sse.js';
38
38
  import { pollUntilTerminal } from '../lib/polling.js';
@@ -87,8 +87,13 @@ async function isStreamTextMockAdvertised(): Promise<boolean> {
87
87
  return Array.isArray(testing?.mockProviders) && testing.mockProviders.includes('stream-text');
88
88
  }
89
89
 
90
- describe.skipIf(SKIP_NO_FIXTURE)('stream-text-fixture: messages-mode fold through the stream-text mock (F1)', () => {
90
+ /** Why the gate below holds, as RFC 0148 §A names it (openwop#1686: a describe-level skip recorded no disposition). */
91
+ const GATE_WHY: readonly [SoftSkipKind, string] =
92
+ (!isFixtureAdvertised(FIXTURE_ID)) ? ['blocked', `the \`${FIXTURE_ID}\` fixture is not advertised`] as const : ['blocked', 'the gate held for no named reason'] as const;
93
+
94
+ describe('stream-text-fixture: messages-mode fold through the stream-text mock (F1)', () => {
91
95
  it('delivers the mocked tokens in order, terminates with isLast + meta, and server-closes', async () => {
96
+ if (SKIP_NO_FIXTURE) return softSkip(...GATE_WHY);
92
97
  if (!(await isStreamTextMockAdvertised())) {
93
98
  // eslint-disable-next-line no-console
94
99
  console.warn(
@@ -191,6 +196,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-text-fixture: messages-mode fold throug
191
196
  });
192
197
 
193
198
  it('an unknown mockProvider.id is rejected with 400 unsupported_mock_provider', async () => {
199
+ if (SKIP_NO_FIXTURE) return softSkip(...GATE_WHY);
194
200
  if (!(await isStreamTextMockAdvertised())) {
195
201
  // eslint-disable-next-line no-console
196
202
  console.warn('[stream-text-fixture] stream-text mock not advertised; skipping negative leg');
@@ -22,7 +22,7 @@ import { subscribe, type SseEvent } from '../lib/sse.js';
22
22
  import { pollUntilTerminal } from '../lib/polling.js';
23
23
  import { isFixtureAdvertised } from '../lib/fixtures.js';
24
24
  import { req } from '../lib/requirement-ids.js';
25
- import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
25
+ import { softSkip, blockedDespiteAssertions, type SoftSkipKind } from '../lib/soft-skip.js';
26
26
 
27
27
  const WORKFLOW_ID = 'conformance-cancellable';
28
28
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
@@ -50,10 +50,15 @@ function getSeq(event: SseEvent): number | null {
50
50
  return null;
51
51
  }
52
52
 
53
- describe.skipIf(SKIP_NO_FIXTURE)('stream-reconnect: Last-Event-ID resume per spec/v1/stream-modes.md', () => {
53
+ /** Why the gate below holds, as RFC 0148 §A names it (openwop#1686: a describe-level skip recorded no disposition). */
54
+ const GATE_WHY: readonly [SoftSkipKind, string] =
55
+ (!isFixtureAdvertised(WORKFLOW_ID)) ? ['blocked', `the \`${WORKFLOW_ID}\` fixture is not advertised`] as const : ['blocked', 'the gate held for no named reason'] as const;
56
+
57
+ describe('stream-reconnect: Last-Event-ID resume per spec/v1/stream-modes.md', () => {
54
58
  it(
55
59
  'reconnect with Last-Event-ID resumes without loss or duplication',
56
60
  async () => {
61
+ if (SKIP_NO_FIXTURE) return softSkip(...GATE_WHY);
57
62
  // Phase 1: kick off a long-running run.
58
63
  const create = await driver.post('/v1/runs', {
59
64
  workflowId: WORKFLOW_ID,
@@ -119,6 +124,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-reconnect: Last-Event-ID resume per spe
119
124
  it(
120
125
  'reconnect with Last-Event-ID equal to terminal seq closes immediately',
121
126
  async () => {
127
+ if (SKIP_NO_FIXTURE) return softSkip(...GATE_WHY);
122
128
  // Quick run, observe terminal seq, then attempt a reconnect after
123
129
  // terminal — server SHOULD close immediately with no events.
124
130
  const create = await driver.post('/v1/runs', { workflowId: 'conformance-noop' });
@@ -21,6 +21,7 @@ import { driver } from '../lib/driver.js';
21
21
  import { pollUntilTerminal } from '../lib/polling.js';
22
22
  import { isFixtureAdvertised } from '../lib/fixtures.js';
23
23
  import { req } from '../lib/requirement-ids.js';
24
+ import { softSkip, type SoftSkipKind } from '../lib/soft-skip.js';
24
25
 
25
26
  const PARENT_WORKFLOW_ID = 'conformance-subworkflow-parent';
26
27
  const CHILD_WORKFLOW_ID = 'conformance-subworkflow-child';
@@ -44,8 +45,14 @@ interface RunEvent {
44
45
  readonly payload?: unknown;
45
46
  }
46
47
 
47
- describe.skipIf(SKIP_NO_FIXTURE)('subworkflow: conformance-subworkflow-parent dispatches child + completes', () => {
48
+ /** Why the gate below holds, as RFC 0148 §A names it (openwop#1686: a describe-level skip recorded no disposition). */
49
+ const GATE_WHY: readonly [SoftSkipKind, string] =
50
+ (!isFixtureAdvertised(PARENT_WORKFLOW_ID)) ? ['blocked', `the \`${PARENT_WORKFLOW_ID}\` fixture is not advertised`] as const :
51
+ (!isFixtureAdvertised(CHILD_WORKFLOW_ID)) ? ['blocked', `the \`${CHILD_WORKFLOW_ID}\` fixture is not advertised`] as const : ['blocked', 'the gate held for no named reason'] as const;
52
+
53
+ describe('subworkflow: conformance-subworkflow-parent dispatches child + completes', () => {
48
54
  it('parent run reaches terminal completed and child variable is propagated via outputMapping', async () => {
55
+ if (SKIP_NO_FIXTURE) return softSkip(...GATE_WHY);
49
56
  const create = await driver.post('/v1/runs', { workflowId: PARENT_WORKFLOW_ID });
50
57
  expect(create.status).toBe(201);
51
58
  const parentRunId = (create.json as { runId: string }).runId;
@@ -68,6 +75,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('subworkflow: conformance-subworkflow-parent di
68
75
  });
69
76
 
70
77
  it('child run is created with parent linkage fields and reaches terminal completed', async () => {
78
+ if (SKIP_NO_FIXTURE) return softSkip(...GATE_WHY);
71
79
  const create = await driver.post('/v1/runs', { workflowId: PARENT_WORKFLOW_ID });
72
80
  const parentRunId = (create.json as { runId: string }).runId;
73
81
 
@@ -0,0 +1,141 @@
1
+ /**
2
+ * `headers.md` §Cross-origin preflight — a host that grants an origin admits
3
+ * what a conforming browser client sends (suite 2.45.0, target major 2;
4
+ * openwop#1763; self-gated on the host's own grant).
5
+ *
6
+ * A browser client MUST send `OpenWOP-Version` and `OpenWOP-Client-Version` on
7
+ * every request (versioning.md §1.3, §1.5), `Authorization` on an
8
+ * authenticated operation and `Content-Type: application/json` with a body.
9
+ * None of them is CORS-safelisted, so the browser preflights. When the host
10
+ * grants the origin but leaves one out of `Access-Control-Allow-Headers`, the
11
+ * browser never sends the request: the operation is unreachable from that
12
+ * origin and the host's logs show nothing. Both production hosts shipped that
13
+ * outage with explicit lists: openwop-app on `OpenWOP-Version` (2026-09-18) and
14
+ * both on `OpenWOP-Client-Version` at the SDK 2.5.0 release (RFC 0219 G8).
15
+ *
16
+ * The admitted set per operation comes from `spec/v2/path-manifest.json`, which
17
+ * `derive-v2-api.py` writes from `api/v2/openapi.yaml`: the declared header
18
+ * parameters, plus `Authorization` when authenticated and `Content-Type` when
19
+ * the operation takes a body. For each operation the leg sends
20
+ * `OPTIONS <path>` (path parameters filled with a placeholder) with `Origin`,
21
+ * `Access-Control-Request-Method` and `Access-Control-Request-Headers`, and
22
+ * reads the answer the way the Fetch standard's CORS-preflight check does:
23
+ *
24
+ * - granted: `Access-Control-Allow-Origin` is the origin or `*`. Anything
25
+ * else and the host does not serve that origin: that operation is not
26
+ * judged (a host MAY grant no origin at all; the rule is conditional).
27
+ * - method: a CORS-safelisted method (GET, HEAD, POST) is admitted without
28
+ * listing; any other must be listed, or `*` when credentials are not
29
+ * allowed. Compared case-sensitively, as Fetch does.
30
+ * - headers: every requested name listed (case-insensitive), or `*` when
31
+ * credentials are not allowed, and `*` never admits `authorization`.
32
+ *
33
+ * The row passes when at least one operation was granted and every granted one
34
+ * admits its set, fails when any granted one does not (naming the operation and
35
+ * the missing names), and is `inapplicable` when no operation was granted. The
36
+ * origin is `OPENWOP_CORS_ORIGIN` (default `https://conformance.invalid`): a
37
+ * host with an allowlist is inapplicable at the default, and its operator sets
38
+ * an allowlisted origin to be witnessed. A pass carries an `observed:` detail.
39
+ *
40
+ * @see spec/v2/core/headers.md §Cross-origin preflight
41
+ */
42
+ import { describe, it, expect } from 'vitest';
43
+ import { readFileSync } from 'node:fs';
44
+ import { join } from 'node:path';
45
+ import { loadEnv } from '../lib/env.js';
46
+ import { v2Discovery } from '../lib/v2.js';
47
+ import { softSkip } from '../lib/soft-skip.js';
48
+ import { req } from '../lib/requirement-ids.js';
49
+ import { noteObservation } from '../lib/row-observation.js';
50
+ import { SCHEMAS_DIR } from '../lib/paths.js';
51
+
52
+ const ID = 'openwop.requirement.headers.cors-preflight-admits';
53
+ const DOC = 'spec/v2/core/headers.md §Cross-origin preflight';
54
+ const ORIGIN_ENV = 'OPENWOP_CORS_ORIGIN';
55
+ const DEFAULT_ORIGIN = 'https://conformance.invalid';
56
+ /** Fetch standard: a CORS-safelisted method passes the preflight method check without being listed. */
57
+ const SAFELISTED_METHODS = new Set(['GET', 'HEAD', 'POST']);
58
+ const PLACEHOLDER = 'conformance-cors-probe';
59
+
60
+ interface ManifestOp { readonly method: string; readonly path: string; readonly operationId: string; readonly requestHeaders?: readonly string[]; readonly authenticated?: boolean; readonly requestBody?: boolean }
61
+ interface Preflight { readonly status: number | null; readonly allowOrigin: string | null; readonly allowMethods: string | null; readonly allowHeaders: string | null; readonly allowCredentials: string | null }
62
+
63
+ function operations(): ManifestOp[] | null {
64
+ try {
65
+ const manifest = JSON.parse(readFileSync(join(SCHEMAS_DIR, '..', 'spec', 'v2', 'path-manifest.json'), 'utf8')) as { operations?: ManifestOp[] };
66
+ const ops = manifest.operations ?? [];
67
+ return ops.length > 0 && ops.every((o) => Array.isArray(o.requestHeaders) && typeof o.authenticated === 'boolean' && typeof o.requestBody === 'boolean') ? ops : null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /** The names a conforming browser client puts in Access-Control-Request-Headers for this operation, lowercased. */
74
+ export function requestedHeaders(op: ManifestOp): string[] {
75
+ const names = new Set((op.requestHeaders ?? []).map((h) => h.toLowerCase()));
76
+ if (op.authenticated === true) names.add('authorization');
77
+ if (op.requestBody === true) names.add('content-type');
78
+ return [...names].sort();
79
+ }
80
+
81
+ const tokens = (v: string | null): string[] => (v ?? '').split(',').map((t) => t.trim()).filter((t) => t.length > 0);
82
+
83
+ /** What the preflight answer fails to admit, per the Fetch standard's CORS-preflight check; empty when it admits everything. */
84
+ export function unadmitted(op: ManifestOp, p: Preflight): { method: string | null; headers: string[] } {
85
+ const credentials = p.allowCredentials === 'true';
86
+ const methods = tokens(p.allowMethods);
87
+ const methodOk = SAFELISTED_METHODS.has(op.method) || methods.includes(op.method) || (!credentials && methods.includes('*'));
88
+ const listed = new Set(tokens(p.allowHeaders).map((h) => h.toLowerCase()));
89
+ const star = !credentials && listed.has('*');
90
+ const headers = requestedHeaders(op).filter((h) => !listed.has(h) && !(star && h !== 'authorization'));
91
+ return { method: methodOk ? null : op.method, headers };
92
+ }
93
+
94
+ async function preflight(baseUrl: string, op: ManifestOp, origin: string): Promise<Preflight> {
95
+ const path = op.path.replace(/\{[^}]+\}/g, PLACEHOLDER);
96
+ try {
97
+ const res = await fetch(`${baseUrl.replace(/\/$/, '')}${path}`, {
98
+ method: 'OPTIONS',
99
+ headers: { Origin: origin, 'Access-Control-Request-Method': op.method, 'Access-Control-Request-Headers': requestedHeaders(op).join(',') },
100
+ });
101
+ await res.arrayBuffer().catch(() => undefined);
102
+ const h = res.headers;
103
+ return { status: res.status, allowOrigin: h.get('access-control-allow-origin'), allowMethods: h.get('access-control-allow-methods'), allowHeaders: h.get('access-control-allow-headers'), allowCredentials: h.get('access-control-allow-credentials') };
104
+ } catch {
105
+ return { status: null, allowOrigin: null, allowMethods: null, allowHeaders: null, allowCredentials: null };
106
+ }
107
+ }
108
+
109
+ describe('headers.md §Cross-origin preflight (self-gated on the host granting the origin)', () => {
110
+ it('every operation a granted origin preflights admits its method and every request header the contract declares for it', async () => {
111
+ if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
112
+ const ops = operations();
113
+ if (ops === null) return softSkip('blocked', 'spec/v2/path-manifest.json carries no per-operation requestHeaders / authenticated / requestBody (a corpus older than openwop#1763), so the admitted sets cannot be derived');
114
+ const origin = process.env[ORIGIN_ENV]?.trim() || DEFAULT_ORIGIN;
115
+ const { baseUrl } = loadEnv();
116
+
117
+ const granted: string[] = [];
118
+ const failures: string[] = [];
119
+ for (const op of ops) {
120
+ const p = await preflight(baseUrl, op, origin);
121
+ if (p.allowOrigin !== origin && p.allowOrigin !== '*') continue;
122
+ granted.push(op.operationId);
123
+ const miss = unadmitted(op, p);
124
+ if (miss.method !== null || miss.headers.length > 0) {
125
+ const parts = [
126
+ ...(miss.method === null ? [] : [`method ${miss.method} not in Access-Control-Allow-Methods (${JSON.stringify(p.allowMethods)})`]),
127
+ ...(miss.headers.length === 0 ? [] : [`${miss.headers.join(', ')} not in Access-Control-Allow-Headers (${JSON.stringify(p.allowHeaders)}${p.allowCredentials === 'true' ? ', credentials allowed so * admits nothing' : ''})`]),
128
+ ];
129
+ failures.push(`${op.operationId} (${op.method} ${op.path}): ${parts.join('; ')}`);
130
+ }
131
+ }
132
+ noteObservation(`origin ${origin}: ${granted.length}/${ops.length} operation(s) granted, ${failures.length} not admitting their set`);
133
+ if (granted.length === 0) {
134
+ return softSkip('inapplicable', `no operation's preflight granted origin ${origin} (Access-Control-Allow-Origin neither it nor *): the host does not serve that origin cross-origin, which is host policy. An operator whose host allowlists origins sets ${ORIGIN_ENV} to one of them to be witnessed`);
135
+ }
136
+ expect(
137
+ failures,
138
+ req(ID, DOC, `a host that grants an origin MUST admit, per operation, its method and every request header api/v2/openapi.yaml declares for it, Authorization when authenticated and Content-Type with a body; a browser that is refused never sends the request. ${failures.length} of ${granted.length} granted operation(s) do not: ${failures.join(' | ')}`),
139
+ ).toEqual([]);
140
+ });
141
+ });
@@ -21,10 +21,13 @@ import { gateFamily, v2Discovery, v2Validator } from '../lib/v2.js';
21
21
  import { readErrorCode } from '../lib/error-envelope.js';
22
22
  import { softSkip } from '../lib/soft-skip.js';
23
23
  import { req } from '../lib/requirement-ids.js';
24
+ import { walkRunList, type FetchPage, type RunListPage, type WalkResult } from '../lib/run-list-walk.js';
24
25
 
25
26
  const DOC = 'spec/v2/core/runs.md §List';
26
27
  const NOOP_WORKFLOW_ID = 'conformance-noop';
27
28
  const MAX_PAGES = 50;
29
+ /** Pages still followed after the created runs are seen, so a host-minted cursor is exercised. */
30
+ const TAIL_PAGES = 2;
28
31
 
29
32
  async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
30
33
  try { return await fn(); } catch { return null; }
@@ -40,22 +43,16 @@ async function createRun(): Promise<{ runId: string } | { reason: string }> {
40
43
 
41
44
  function tenantOf(runId: string): string { return runId.slice(0, runId.indexOf('/')); }
42
45
 
43
- type Page = { runs?: unknown; nextCursor?: unknown };
44
-
45
- /** Walk the list from the first page, collecting run ids, until the last page or MAX_PAGES. */
46
- async function walk(query: string, maxPageSize: number): Promise<{ ids: string[]; pages: Page[]; reason?: string }> {
47
- const ids: string[] = []; const pages: Page[] = []; let cursor: string | undefined;
48
- for (let i = 0; i < MAX_PAGES; i++) {
46
+ /** The newest-first, stop-when-found walk (`lib/run-list-walk.ts`) over this host's `GET /runs`. */
47
+ async function walk(query: string, maxPageSize: number, want: string[]): Promise<WalkResult> {
48
+ const fetchPage: FetchPage = async (cursor) => {
49
49
  const q = `${query}${query ? '&' : '?'}limit=${maxPageSize}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`;
50
50
  const res = await http(() => driver.get(`/runs${q}`));
51
- if (res === null) return { ids, pages, reason: `GET /runs${q} unreachable (fetch failed)` };
52
- if (res.status !== 200) return { ids, pages, reason: `GET /runs${q} answered ${res.status} ${readErrorCode(res.json) ?? ''}` };
53
- const page = res.json as Page; pages.push(page);
54
- for (const r of Array.isArray(page.runs) ? page.runs : []) { const id = (r as { runId?: unknown }).runId; if (typeof id === 'string') ids.push(id); }
55
- if (typeof page.nextCursor !== 'string') break;
56
- cursor = page.nextCursor;
57
- }
58
- return { ids, pages };
51
+ if (res === null) return { reason: `GET /runs${q} unreachable (fetch failed)` };
52
+ if (res.status !== 200) return { reason: `GET /runs${q} answered ${res.status} ${readErrorCode(res.json) ?? ''}` };
53
+ return { page: res.json as RunListPage };
54
+ };
55
+ return walkRunList(fetchPage, { want, maxPages: MAX_PAGES, tailPages: TAIL_PAGES });
59
56
  }
60
57
 
61
58
  describe('RFC 0182 — run-list (gated on runList)', () => {
@@ -68,7 +65,7 @@ describe('RFC 0182 — run-list (gated on runList)', () => {
68
65
  const a = await createRun(); if ('reason' in a) return softSkip('blocked', a.reason);
69
66
  const b = await createRun(); if ('reason' in b) return softSkip('blocked', b.reason);
70
67
  const tenant = tenantOf(a.runId);
71
- const walked = await walk('', maxPageSize);
68
+ const walked = await walk('', maxPageSize, [a.runId, b.runId]);
72
69
  if (walked.reason && walked.pages.length === 0) return softSkip('blocked', walked.reason);
73
70
  const validate = v2Validator('run-list-response');
74
71
  for (const page of walked.pages) {
@@ -77,6 +74,7 @@ describe('RFC 0182 — run-list (gated on runList)', () => {
77
74
  expect((page.runs as unknown[]).length <= maxPageSize, req('openwop.requirement.0182.run-list.tenant-scoped', DOC, `a page MUST NOT exceed the advertised runList.maxPageSize (${maxPageSize}); got ${(page.runs as unknown[]).length}`)).toBe(true);
78
75
  }
79
76
  expect(walked.ids.includes(a.runId) && walked.ids.includes(b.runId), req('openwop.requirement.0182.run-list.tenant-scoped', DOC, `a run the caller just created MUST appear in its unfiltered list — created ${a.runId} and ${b.runId}; walked ${walked.ids.length} id(s) over ${walked.pages.length} page(s)${walked.reason ? ` (${walked.reason})` : ''}`)).toBe(true);
77
+ expect(walked.ids.indexOf(b.runId) < walked.ids.indexOf(a.runId), req('openwop.requirement.0182.run-list.tenant-scoped', DOC, `the list MUST be newest first — ${b.runId} was created after ${a.runId}, so it MUST be listed before it`)).toBe(true);
80
78
  const foreign = walked.ids.filter((id) => tenantOf(id) !== tenant);
81
79
  expect(foreign, req('openwop.requirement.0182.run-list.tenant-scoped', 'spec/v2/core/identity.md §5', `every runId in the list MUST carry the caller's tenant segment (${tenant}) — the list is tenant-scoped by construction; found ${foreign.length} other(s): ${foreign.slice(0, 3).join(', ')}`)).toEqual([]);
82
80
  });
@@ -98,7 +96,7 @@ describe('RFC 0182 — run-list (gated on runList)', () => {
98
96
  if (!filters.includes('workflowId')) return softSkip('inapplicable', 'runList.filters does not name workflowId — the filter leg has no obligation');
99
97
  const maxPageSize = typeof fam['maxPageSize'] === 'number' ? fam['maxPageSize'] : 1;
100
98
  const c = await createRun(); if ('reason' in c) return softSkip('blocked', c.reason);
101
- const walked = await walk(`?workflowId=${encodeURIComponent(NOOP_WORKFLOW_ID)}`, maxPageSize);
99
+ const walked = await walk(`?workflowId=${encodeURIComponent(NOOP_WORKFLOW_ID)}`, maxPageSize, [c.runId]);
102
100
  if (walked.reason && walked.pages.length === 0) return softSkip('blocked', walked.reason);
103
101
  const others = walked.pages.flatMap((p) => (Array.isArray(p.runs) ? p.runs : [])).filter((r) => (r as { workflowId?: unknown }).workflowId !== NOOP_WORKFLOW_ID);
104
102
  expect(others.length, req('openwop.requirement.0182.run-list.filter-exact', DOC, `a workflowId filter MUST be exact — every listed run's workflowId MUST equal ${NOOP_WORKFLOW_ID}; found ${others.length} other(s)`)).toBe(0);