@openwop/openwop-conformance 2.42.2 → 2.42.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +1 -1
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +353 -133
- package/schemas/CORPUS-STAMP.json +10 -10
- package/src/lib/agentOrgChart.ts +21 -4
- package/src/lib/effect-receiver.ts +23 -2
- package/src/lib/toolCatalog.ts +29 -5
- package/src/scenarios/agent-manifest-runtime.test.ts +17 -4
- package/src/scenarios/agent-org-chart-scoping.test.ts +27 -4
- package/src/scenarios/agent-roster-attribution.test.ts +28 -4
- package/src/scenarios/agentPackExport.test.ts +33 -0
- package/src/scenarios/agentPackInstall.test.ts +32 -0
- package/src/scenarios/agentReasoningEvents.test.ts +22 -13
- package/src/scenarios/agents-run-tool-allowlist.test.ts +6 -2
- package/src/scenarios/ai-envelope-shape.test.ts +9 -8
- package/src/scenarios/anonymous-actor-write-gated.test.ts +12 -6
- package/src/scenarios/audit-log-integrity.test.ts +37 -0
- package/src/scenarios/context-budget-transcript-bound.test.ts +14 -4
- package/src/scenarios/context-summarization-replay.test.ts +17 -8
- package/src/scenarios/conversationLifecycle.test.ts +12 -1
- package/src/scenarios/conversationReplayDeterminism.test.ts +19 -3
- package/src/scenarios/cost-attribution.test.ts +12 -0
- package/src/scenarios/debug-bundle-truncation.test.ts +16 -3
- package/src/scenarios/distillation-shape.test.ts +30 -0
- package/src/scenarios/envelope-truncated.test.ts +6 -2
- package/src/scenarios/feedback-fork-not-copied.test.ts +28 -7
- package/src/scenarios/idempotency-concurrent-claim.test.ts +27 -9
- package/src/scenarios/idempotency-key-determinism.test.ts +6 -2
- package/src/scenarios/inbound-credential-no-passthrough.test.ts +13 -2
- package/src/scenarios/mcp-server-untrusted-args.test.ts +20 -5
- package/src/scenarios/mcp-tool-roundtrip.test.ts +7 -21
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +11 -3
- package/src/scenarios/orchestratorDispatch.test.ts +18 -2
- package/src/scenarios/pack-registry.test.ts +8 -1
- package/src/scenarios/production-backpressure.test.ts +17 -11
- package/src/scenarios/redaction.test.ts +8 -2
- package/src/scenarios/replay-fanout-suppression.test.ts +18 -2
- package/src/scenarios/runner-ledger.test.ts +7 -2
- package/src/scenarios/stream-modes.test.ts +27 -0
- package/src/scenarios/tool-catalog-projection.test.ts +35 -12
- package/src/scenarios/v2-advertised-fixtures-exist.test.ts +17 -3
- package/src/scenarios/v2-artifact-a2a-shape.test.ts +8 -2
- package/src/scenarios/v2-bound-id-kinds.test.ts +13 -4
- package/src/scenarios/v2-effect-identity-business-key.test.ts +71 -13
- package/src/scenarios/v2-effect-seam-manifest.test.ts +3 -9
- package/src/scenarios/v2-era-2-append-vocabulary.test.ts +52 -9
- package/src/scenarios/v2-idempotency-in-flight.test.ts +16 -0
- package/src/scenarios/v2-interop-trace-context.test.ts +23 -0
- package/src/scenarios/v2-mcp-tasks.test.ts +33 -6
- package/src/scenarios/v2-min-client-version.test.ts +11 -1
- package/src/scenarios/v2-mrtr-rounds-ceiling.test.ts +11 -0
- package/src/scenarios/v2-poll-cursor-v2.test.ts +28 -5
- package/src/scenarios/v2-profiles-derived-only.test.ts +9 -2
- package/src/scenarios/v2-run-diff-identical.test.ts +13 -5
- package/src/scenarios/v2-run-pause-resume.test.ts +4 -1
- package/src/scenarios/v2-sse-last-event-id.test.ts +7 -0
- package/src/scenarios/v2-version-header-honored.test.ts +22 -3
- package/src/scenarios/v2-webhook-delivery-isolation.test.ts +28 -8
- package/src/scenarios/v2-webhook-durable-delivery.test.ts +24 -0
- package/src/scenarios/v2-webhook-endpoint-verification.test.ts +25 -6
- package/src/scenarios/v2-webhook-message-id-stable.test.ts +10 -2
- package/src/scenarios/v2-webhook-unregister-stops-delivery.test.ts +6 -1
- package/src/scenarios/version-negotiation.test.ts +12 -1
- package/src/scenarios/webhook-signed-delivery.test.ts +18 -3
|
@@ -194,7 +194,13 @@ describe('mcp-tool-roundtrip: server wire shape', () => {
|
|
|
194
194
|
);
|
|
195
195
|
return softSkip('blocked', 'precondition not met — `!probe` returned early (seam, prior step, or fixture unavailable)');
|
|
196
196
|
}
|
|
197
|
-
|
|
197
|
+
// unfailable-leg audit wave 2, 2026-09-27: with only the in-process fake
|
|
198
|
+
// configured, this leg probed the SUITE'S OWN fake MCP server and recorded
|
|
199
|
+
// `executed-pass` — every host passed it, conforming or not, because the
|
|
200
|
+
// host under test was never contacted. The fake-path self-check is now
|
|
201
|
+
// `inapplicable` before any assertion (the host-mediated leg below is the
|
|
202
|
+
// one that exercises the host against the fake).
|
|
203
|
+
if (!probe.isReal) return softSkip('inapplicable', 'suite fixture self-test — host not exercised (only the in-process MCP fake is configured; set OPENWOP_MCP_REAL_SERVER_URL for real-server interop evidence)');
|
|
198
204
|
|
|
199
205
|
// Per MCP `initialize` spec, params MUST carry protocolVersion +
|
|
200
206
|
// capabilities + clientInfo. The in-process fake accepts empty
|
|
@@ -258,26 +264,6 @@ describe('mcp-tool-roundtrip: server wire shape', () => {
|
|
|
258
264
|
`[mcp-tool-roundtrip] real-server interop OK against ${probe.url} ` +
|
|
259
265
|
`(tool=${first?.name}, isError=${callResult.isError === true})`,
|
|
260
266
|
);
|
|
261
|
-
} else {
|
|
262
|
-
// Fake-server path: deterministic echo tool, assert verbatim.
|
|
263
|
-
expect(listResult.tools?.some((t) => t.name === 'echo')).toBe(true);
|
|
264
|
-
|
|
265
|
-
const call = await postJsonRpc(
|
|
266
|
-
probe.url,
|
|
267
|
-
'tools/call',
|
|
268
|
-
{ name: 'echo', arguments: { text: 'hello-from-conformance' } },
|
|
269
|
-
3,
|
|
270
|
-
);
|
|
271
|
-
expect(call.status).toBe(200);
|
|
272
|
-
const callResult = (call.json.result ?? {}) as {
|
|
273
|
-
content?: ReadonlyArray<{ type?: string; text?: string }>;
|
|
274
|
-
};
|
|
275
|
-
expect(callResult.content?.[0]?.type).toBe('text');
|
|
276
|
-
expect(callResult.content?.[0]?.text).toBe('hello-from-conformance');
|
|
277
|
-
|
|
278
|
-
const fake = getMcpFakeServer()!;
|
|
279
|
-
const methods = fake.invocations().map((i) => i.method);
|
|
280
|
-
expect(methods).toEqual(['initialize', 'tools/list', 'tools/call']);
|
|
281
267
|
}
|
|
282
268
|
});
|
|
283
269
|
});
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
22
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
23
23
|
import { driver } from '../lib/driver.js';
|
|
24
24
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
25
25
|
import { req } from '../lib/requirement-ids.js';
|
|
@@ -90,7 +90,11 @@ describe('memory-compaction-provenance-tag: compacted-from:<id> tag follows §C
|
|
|
90
90
|
if (listRes.status === 404) {
|
|
91
91
|
// eslint-disable-next-line no-console
|
|
92
92
|
console.warn('[rfc0012-tag] host does not expose memory:list at /v1/memory/{ref}; skipping tag inspection (canonical provenance signal remains the memory.compacted event itself)');
|
|
93
|
-
|
|
93
|
+
// unfailable-leg audit wave 2, 2026-09-27: this return follows the seed/
|
|
94
|
+
// compact asserts, so a plain softSkip recorded a partial-witness
|
|
95
|
+
// `executed-pass` — a host whose compacted entry was never inspectable
|
|
96
|
+
// passed the tag requirement unobserved. Now `blocked`.
|
|
97
|
+
return blockedDespiteAssertions('[rfc0012-tag] host does not expose memory:list at /v1/memory/{ref}; skipping tag inspection (canonical provenance signal remains the memory.compacted event itself)');
|
|
94
98
|
}
|
|
95
99
|
expect(listRes.status, req('openwop.it.memory-compaction-provenance-tag.compacted-entry-carries-a-well-formed-compacted-from-tag-or-omits-it-cleanly-no', 'RFC 0012 §C', 'memory:list MUST return 200 when reachable')).toBe(200);
|
|
96
100
|
|
|
@@ -100,7 +104,11 @@ describe('memory-compaction-provenance-tag: compacted-from:<id> tag follows §C
|
|
|
100
104
|
if (!output) {
|
|
101
105
|
// eslint-disable-next-line no-console
|
|
102
106
|
console.warn(`[rfc0012-tag] outputId ${outputId} not visible via memory:list; cannot inspect tags`);
|
|
103
|
-
|
|
107
|
+
// unfailable-leg audit wave 2, 2026-09-27: this return follows the seed/
|
|
108
|
+
// compact asserts, so a plain softSkip recorded a partial-witness
|
|
109
|
+
// `executed-pass` — a host whose compacted entry was never inspectable
|
|
110
|
+
// passed the tag requirement unobserved. Now `blocked`.
|
|
111
|
+
return blockedDespiteAssertions('[rfc0012-tag] outputId … not visible via memory:list; cannot inspect tags');
|
|
104
112
|
}
|
|
105
113
|
const tags = output.tags ?? [];
|
|
106
114
|
|
|
@@ -45,8 +45,8 @@ describe.skipIf(SKIP)('orchestratorDispatch: supervisor → dispatch → next-wo
|
|
|
45
45
|
expect(terminal.status).toBe('completed');
|
|
46
46
|
|
|
47
47
|
const events = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
|
|
48
|
-
const list = (events.json as { events?: Array<{ type: string; payload?: Record<string, unknown> }> })
|
|
49
|
-
.events ?? [];
|
|
48
|
+
const list = ((events.json as { events?: Array<{ type: string; sequence?: number; nodeId?: string; payload?: Record<string, unknown> }> })
|
|
49
|
+
.events ?? []).slice().sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0));
|
|
50
50
|
|
|
51
51
|
const decisions = list.filter((e) => e.type === 'runOrchestrator.decided');
|
|
52
52
|
expect(decisions.length).toBeGreaterThan(0);
|
|
@@ -64,5 +64,21 @@ describe.skipIf(SKIP)('orchestratorDispatch: supervisor → dispatch → next-wo
|
|
|
64
64
|
expect(decision.kind).toBe('next-worker');
|
|
65
65
|
expect(Array.isArray(decision.nextWorkerIds)).toBe(true);
|
|
66
66
|
expect(decision.nextWorkerIds.length).toBeGreaterThanOrEqual(1);
|
|
67
|
+
|
|
68
|
+
// unfailable-leg audit wave 2, 2026-09-27: the leg is named "decided
|
|
69
|
+
// {next-worker} BETWEEN supervisor + dispatch" but never read an order —
|
|
70
|
+
// a host that emitted the decision AFTER the dispatch node had already
|
|
71
|
+
// started (or never started the dispatch node at all) passed. The
|
|
72
|
+
// next-worker decision MUST precede the dispatch node's first node.started.
|
|
73
|
+
const nodeIdOf = (e: { nodeId?: string; payload?: Record<string, unknown> }): unknown => e.nodeId ?? e.payload?.nodeId;
|
|
74
|
+
const dispatchStarted = list.find((e) => e.type === 'node.started' && nodeIdOf(e) === 'dispatch');
|
|
75
|
+
expect(
|
|
76
|
+
dispatchStarted,
|
|
77
|
+
req('openwop.it.orchestratorDispatch.emits-runorchestrator-decided-next-worker-between-supervisor-dispatch', 'RFCS/0006-orchestrator.md', 'the dispatch node MUST emit node.started after the supervisor decides next-worker'),
|
|
78
|
+
).toBeDefined();
|
|
79
|
+
expect(
|
|
80
|
+
typeof nextWorker!.sequence === 'number' && typeof dispatchStarted?.sequence === 'number' && nextWorker!.sequence < dispatchStarted.sequence,
|
|
81
|
+
req('openwop.it.orchestratorDispatch.emits-runorchestrator-decided-next-worker-between-supervisor-dispatch', 'RFCS/0006-orchestrator.md', 'runOrchestrator.decided{next-worker} MUST precede node.started{dispatch} in event-log sequence'),
|
|
82
|
+
).toBe(true);
|
|
67
83
|
});
|
|
68
84
|
});
|
|
@@ -208,7 +208,14 @@ describe('pack-registry: read-endpoint shape contracts', () => {
|
|
|
208
208
|
'node-packs.md §"GET /v1/packs/{name}/-/{version}.sig"',
|
|
209
209
|
'404 response MUST carry a string `error` field — `signature_not_available` is the canonical code',
|
|
210
210
|
)).toBe('string');
|
|
211
|
-
|
|
211
|
+
// unfailable-leg audit wave 2, 2026-09-27: any non-empty `error` string
|
|
212
|
+
// (e.g. `not_found`) previously passed; node-packs.md names
|
|
213
|
+
// `404 signature_not_available` for all four cases (missing / yanked /
|
|
214
|
+
// unsigned / storage-unwired), intentionally indistinguishable.
|
|
215
|
+
expect(body?.error, req('openwop.it.pack-registry.get-v1-packs-name-version-sig-returns-404-signature-not-available-for-nonexisten',
|
|
216
|
+
'node-packs.md §"GET /v1/packs/{name}/-/{version}.sig"',
|
|
217
|
+
'nonexistent (name, version) MUST return 404 `signature_not_available`',
|
|
218
|
+
)).toBe('signature_not_available');
|
|
212
219
|
});
|
|
213
220
|
|
|
214
221
|
it('GET /v1/packs/{bad-name}/-/{version}.json returns 400 invalid_pack_name', async () => {
|
|
@@ -42,7 +42,7 @@ import { behaviorGate } from '../lib/behavior-gate.js';
|
|
|
42
42
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
43
43
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
44
44
|
import { req } from '../lib/requirement-ids.js';
|
|
45
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
45
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
46
46
|
|
|
47
47
|
interface BackpressureCaps {
|
|
48
48
|
supported?: boolean;
|
|
@@ -212,7 +212,10 @@ describe('production-backpressure: 503 envelope under saturation', () => {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
await Promise.allSettled(slotPromises);
|
|
215
|
-
|
|
215
|
+
// unfailable-leg audit wave 2, 2026-09-27: this plain softSkip came
|
|
216
|
+
// AFTER up to cap-1 `create.status === 201` assertions, so the row was
|
|
217
|
+
// a partial-witness PASS although no 503 envelope was ever observed.
|
|
218
|
+
return blockedDespiteAssertions('inflight cap already saturated by parallel runs before the cap+1 probe (run with --no-file-parallelism) — 503 envelope not observed');
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
// Let SSE connections register.
|
|
@@ -233,7 +236,10 @@ describe('production-backpressure: 503 envelope under saturation', () => {
|
|
|
233
236
|
console.warn(
|
|
234
237
|
`[production-backpressure] expected 503 at cap+1=${cap + 1}, got ${blocked.status}; saturation may need tuning`,
|
|
235
238
|
);
|
|
236
|
-
|
|
239
|
+
// unfailable-leg audit wave 2, 2026-09-27: after `cap` × 201 asserts a
|
|
240
|
+
// plain softSkip recorded a partial-witness PASS — a host that never
|
|
241
|
+
// 503s at cap+1 passed the 503-envelope leg. Now `blocked`.
|
|
242
|
+
return blockedDespiteAssertions(`expected 503 at cap+1=${cap + 1}, got ${blocked.status} — saturation not reached (SSE slots may not have registered); 503 envelope not observed`);
|
|
237
243
|
}
|
|
238
244
|
|
|
239
245
|
const retryAfterHeader = blocked.headers.get('retry-after');
|
|
@@ -318,14 +324,14 @@ describe('production-backpressure: discovery exempt from cap', () => {
|
|
|
318
324
|
|
|
319
325
|
const cap = prod?.backpressure?.inflightCap;
|
|
320
326
|
if (cap === undefined) {
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return softSkip('inapplicable', '
|
|
327
|
+
// unfailable-leg audit wave 2, 2026-09-27: this used to take one UNLOADED
|
|
328
|
+
// discovery probe and then soft-skip, which recorded a partial-witness PASS
|
|
329
|
+
// for "returns 200 even when inflight is saturated", although nothing was
|
|
330
|
+
// saturated. The cap is OPTIONAL, so a host without one has nothing to
|
|
331
|
+
// saturate and the requirement does not bind it: `inapplicable`, before
|
|
332
|
+
// any assertion. It is not `blocked`, which would deny certification to a
|
|
333
|
+
// conforming host for a surface it may decline.
|
|
334
|
+
return softSkip('inapplicable', 'host does not advertise production.backpressure.inflightCap — there is no cap to saturate, so discovery-under-saturation does not bind');
|
|
329
335
|
}
|
|
330
336
|
|
|
331
337
|
// Issue many concurrent discovery probes; all MUST succeed.
|
|
@@ -187,6 +187,9 @@ describe('redaction: 401 response MUST NOT echo invalid Bearer token (NFR-7)', (
|
|
|
187
187
|
|
|
188
188
|
describe.skipIf(SKIP_NO_NOOP)('redaction: credentialRef value MUST NOT appear in event payloads (gated on secrets.supported)', () => {
|
|
189
189
|
it('skips when host does NOT advertise secrets.supported', async () => {
|
|
190
|
+
// Names the row id first (the registry takes the FIRST req() in a body as
|
|
191
|
+
// the explicitId) now that the tautological assert below is gone.
|
|
192
|
+
req('openwop.it.redaction.skips-when-host-does-not-advertise-secrets-supported', 'capabilities.md', 'a credentialRef value MUST NOT appear in any RunEvent payload');
|
|
190
193
|
const cap = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
191
194
|
const supported =
|
|
192
195
|
(cap.json as { secrets?: { supported?: boolean } } | undefined)?.secrets
|
|
@@ -194,8 +197,11 @@ describe.skipIf(SKIP_NO_NOOP)('redaction: credentialRef value MUST NOT appear in
|
|
|
194
197
|
|
|
195
198
|
if (supported !== true) {
|
|
196
199
|
// Spec-allowed — this scenario only applies to hosts that opt
|
|
197
|
-
// into BYOK.
|
|
198
|
-
expect(supported
|
|
200
|
+
// into BYOK. unfailable-leg audit wave 2, 2026-09-27: a tautological
|
|
201
|
+
// `expect(supported).not.toBe(true)` inside `if (supported !== true)`
|
|
202
|
+
// turned every non-BYOK host's row into an executed PASS of the
|
|
203
|
+
// credentialRef no-leak requirement; with zero assertions the row now
|
|
204
|
+
// records `inapplicable`.
|
|
199
205
|
return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `supported !== true` returned early');
|
|
200
206
|
}
|
|
201
207
|
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
|
|
89
89
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
90
90
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
91
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
91
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
92
92
|
import { driver } from '../lib/driver.js';
|
|
93
93
|
import { forkDeclined } from '../lib/fork-availability.js';
|
|
94
94
|
import { discoveryFamilies, readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
@@ -313,7 +313,7 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
|
|
|
313
313
|
}
|
|
314
314
|
expect(replay.status, req('openwop.it.replay-fanout-suppression.delivers-for-a-live-run-suppresses-for-a-replay-fork-and-delivers-again-for-a-br', 'webhooks.md §"Register"', 'replay fork should be accepted')).toBe(201);
|
|
315
315
|
const replayRunId = (replay.json as { runId: string }).runId;
|
|
316
|
-
await pollUntilTerminal(replayRunId, { timeoutMs: FORK_POLL_MS });
|
|
316
|
+
const replayTerminal = await pollUntilTerminal(replayRunId, { timeoutMs: FORK_POLL_MS });
|
|
317
317
|
await quietWindow(SUPPRESSION_WINDOW_MS);
|
|
318
318
|
|
|
319
319
|
expect(
|
|
@@ -342,6 +342,22 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
|
|
|
342
342
|
),
|
|
343
343
|
).toBeGreaterThan(0);
|
|
344
344
|
|
|
345
|
+
// unfailable-leg audit wave 2, 2026-09-27: a non-empty fork log was the
|
|
346
|
+
// only re-emission check, so a replay fork that FAILED (or never reached
|
|
347
|
+
// `run.completed`, the one event the webhook subscribes to) delivered
|
|
348
|
+
// nothing because it never re-emitted the subscribed event — and was
|
|
349
|
+
// recorded `executed-pass` for the suppression MUST NOT. The fork must
|
|
350
|
+
// terminate `completed` with `run.completed` in its own log before the
|
|
351
|
+
// absence of deliveries is evidence; otherwise `blocked`.
|
|
352
|
+
const forkReemittedCompleted = forkEventList.some((e) => e.type === 'run.completed');
|
|
353
|
+
if (replayTerminal.status !== 'completed' || !forkReemittedCompleted) {
|
|
354
|
+
const why = `replay fork ${replayRunId} terminated ${String(replayTerminal.status)}`
|
|
355
|
+
+ `${forkReemittedCompleted ? '' : ' without run.completed in its event log'} — the subscribed event was never re-emitted, `
|
|
356
|
+
+ 'so the absence of deliveries proves nothing';
|
|
357
|
+
recordRequirement(REQUIREMENT_ID, 'blocked', why);
|
|
358
|
+
return blockedDespiteAssertions(why);
|
|
359
|
+
}
|
|
360
|
+
|
|
345
361
|
// The MUST NOT has now been exercised against wiring proven to deliver, on a
|
|
346
362
|
// fork proven to have re-emitted. Recorded here rather than after leg 3,
|
|
347
363
|
// because leg 3 pins the boundary and may legitimately not run.
|
|
@@ -270,11 +270,16 @@ describe('RFC 0148 §A (S6) — the live sink, when this run was given one', ()
|
|
|
270
270
|
it('files that finished before this one appear in the ledger file with an assertion count', () => {
|
|
271
271
|
const path = process.env['OPENWOP_LEDGER_PATH'];
|
|
272
272
|
if (!path || !existsSync(path)) return softSkip('blocked', 'precondition not met — `!path || !existsSync(path)` returned early (not a --certify run; nothing to inspect) (seam, prior step, or fixture unavailable)'); // not a --certify run; nothing to inspect
|
|
273
|
-
const entries = readLedgerFile(path);
|
|
274
273
|
// At least the in-memory ledger of THIS worker has file-level entries for
|
|
275
274
|
// earlier files (setup.ts afterAll), and each carries assertionCount.
|
|
275
|
+
//
|
|
276
|
+
// unfailable-leg audit wave 2, 2026-09-27: the leg ended with
|
|
277
|
+
// `expect(entries.length).toBeGreaterThanOrEqual(0)` — a tautology (and a
|
|
278
|
+
// bare expect with no req()) that made the leg an `executed-pass` even when
|
|
279
|
+
// `fileEntries` was empty and the loop asserted nothing. Removed; an empty
|
|
280
|
+
// set now says so instead of passing vacuously.
|
|
276
281
|
const fileEntries = snapshot().filter((e) => e.requirementId.startsWith('openwop.scenario.') || e.requirementId.startsWith('openwop.floor.'));
|
|
282
|
+
if (fileEntries.length === 0) return softSkip('inapplicable', 'no file-level ledger entries recorded in this worker before this leg — nothing to inspect');
|
|
277
283
|
for (const e of fileEntries) expect(typeof e.assertionCount, req('openwop.it.runner-ledger.files-that-finished-before-this-one-appear-in-the-ledger-file-with-an-assertion', 'RFC 0148 §A', 'files that finished before this one appear in the ledger file with an assertion count')).toBe('number');
|
|
278
|
-
expect(entries.length).toBeGreaterThanOrEqual(0);
|
|
279
284
|
});
|
|
280
285
|
});
|
|
@@ -132,6 +132,33 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes: debug emits at least as many eve
|
|
|
132
132
|
'debug stream event count MUST be >= updates stream event count',
|
|
133
133
|
)).toBeGreaterThanOrEqual(updatesResult.events.length);
|
|
134
134
|
|
|
135
|
+
// unfailable-leg audit wave 2, 2026-09-27: superset-by-COUNT alone passed a
|
|
136
|
+
// host whose debug stream dropped canonical types (e.g. no run.completed)
|
|
137
|
+
// as long as it padded the count with other frames (log.appended,
|
|
138
|
+
// keep-alive-ish events). The mapping table is per TYPE: every type the
|
|
139
|
+
// updates stream carried MUST appear in debug, and debug MUST carry
|
|
140
|
+
// node.started (✅ debug / — updates) — the conformance-delay fixture has
|
|
141
|
+
// one node (`wait`), so a debug stream without node.started is filtering.
|
|
142
|
+
const typeOf = (e: SseEvent): string => {
|
|
143
|
+
if (e.event !== 'message') return e.event;
|
|
144
|
+
try {
|
|
145
|
+
const t = (JSON.parse(e.data) as { type?: unknown }).type;
|
|
146
|
+
return typeof t === 'string' ? t : e.event;
|
|
147
|
+
} catch {
|
|
148
|
+
return e.event;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const debugTypes = new Set(debugResult.events.map(typeOf));
|
|
152
|
+
const missingFromDebug = [...new Set(updatesResult.events.map(typeOf))].filter((t) => !debugTypes.has(t));
|
|
153
|
+
expect(missingFromDebug, req('openwop.it.stream-modes.debug-stream-is-a-superset-of-updates-per-stream-modes-md-mode-mapping',
|
|
154
|
+
'stream-modes.md §Mode-to-event mapping',
|
|
155
|
+
'every event type emitted in updates mode is also ✅ in debug mode — debug MUST carry each of them',
|
|
156
|
+
)).toEqual([]);
|
|
157
|
+
expect([...debugTypes], req('openwop.it.stream-modes.debug-stream-is-a-superset-of-updates-per-stream-modes-md-mode-mapping',
|
|
158
|
+
'stream-modes.md §Mode-to-event mapping',
|
|
159
|
+
'debug mode MUST emit node.started (✅ debug) for the fixture\'s node',
|
|
160
|
+
)).toContain('node.started');
|
|
161
|
+
|
|
135
162
|
expect(debugResult.closedBy, req('openwop.it.stream-modes.debug-stream-is-a-superset-of-updates-per-stream-modes-md-mode-mapping',
|
|
136
163
|
'stream-modes.md §debug',
|
|
137
164
|
'debug stream MUST close on terminal event',
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
TOOL_CONTENT_FORBIDDEN,
|
|
56
56
|
} from '../lib/toolCatalog.js';
|
|
57
57
|
import { req } from '../lib/requirement-ids.js';
|
|
58
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
58
|
+
import { softSkip, seamAbsent } from '../lib/soft-skip.js';
|
|
59
59
|
|
|
60
60
|
function loadSchema(name: string): Record<string, unknown> {
|
|
61
61
|
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
|
|
@@ -92,8 +92,23 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
|
|
|
92
92
|
).toBe(true);
|
|
93
93
|
|
|
94
94
|
// ---- Leg 1: the list (§B) -------------------------------------------
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// unfailable-leg audit wave 2, 2026-09-27: a host that ADVERTISES the
|
|
96
|
+
// catalog but 404s the list (or answers 500 / a non-list 200, which the
|
|
97
|
+
// old `listTools` read as `[]`) previously recorded a partial-witness pass
|
|
98
|
+
// after the 401 assertion. An unserved read is now `seamAbsent` (blocked;
|
|
99
|
+
// fails under OPENWOP_REQUIRE_BEHAVIOR=true), and any other non-200 or
|
|
100
|
+
// non-list body FAILS.
|
|
101
|
+
const read = await listTools();
|
|
102
|
+
if (read.unserved) return seamAbsent(`host advertises toolCatalog but ${toolsPath()} answered ${read.status}`);
|
|
103
|
+
expect(
|
|
104
|
+
read.status,
|
|
105
|
+
req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', `GET ${toolsPath()} MUST return 200 to an authenticated caller on a host advertising toolCatalog`),
|
|
106
|
+
).toBe(200);
|
|
107
|
+
expect(
|
|
108
|
+
read.tools !== null,
|
|
109
|
+
req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', `GET ${toolsPath()} MUST return a ToolDescriptor[]`),
|
|
110
|
+
).toBe(true);
|
|
111
|
+
const tools = read.tools ?? [];
|
|
97
112
|
|
|
98
113
|
for (const t of tools) {
|
|
99
114
|
const v = validate(t);
|
|
@@ -116,12 +131,18 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
|
|
|
116
131
|
if (tools.length > 0 && typeof tools[0]!.toolId === 'string') {
|
|
117
132
|
const id = tools[0]!.toolId as string;
|
|
118
133
|
const one = await getTool(id);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
// unfailable-leg audit wave 2, 2026-09-27: the by-id check used to run
|
|
135
|
+
// only `if (one.status === 200)`, so a host that 404'd (or 500'd) the
|
|
136
|
+
// by-id read of a tool its own list returned passed. A listed tool is
|
|
137
|
+
// one the caller may see; its by-id read MUST serve it.
|
|
138
|
+
expect(
|
|
139
|
+
one.status,
|
|
140
|
+
req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', 'GET /v1/tools/{toolId} MUST return 200 for a toolId the caller\'s own list returned'),
|
|
141
|
+
).toBe(200);
|
|
142
|
+
expect(
|
|
143
|
+
one.descriptor?.toolId === id,
|
|
144
|
+
req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', 'GET /v1/tools/{toolId} MUST return the requested descriptor'),
|
|
145
|
+
).toBe(true);
|
|
125
146
|
}
|
|
126
147
|
const unknown = await getTool('__conformance_nonexistent_tool__');
|
|
127
148
|
expect(
|
|
@@ -142,9 +163,11 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
|
|
|
142
163
|
|
|
143
164
|
it('advisory (SHOULD): an unchanged catalog reads identically, sorted by toolId', async () => {
|
|
144
165
|
if (!(await toolCatalogGate('openwop-tool-catalog'))) return;
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
166
|
+
const ra = await listTools();
|
|
167
|
+
const rb = await listTools();
|
|
168
|
+
const a = ra.tools;
|
|
169
|
+
const b = rb.tools;
|
|
170
|
+
if (a === null || b === null) return softSkip('inapplicable', `the list read did not return a ToolDescriptor[] (status ${ra.status}/${rb.status}); the normative leg above records that — this advisory leg is never failed`);
|
|
148
171
|
const ids = (l: ReadonlyArray<{ toolId?: unknown }>) => l.map((t) => String(t.toolId));
|
|
149
172
|
const first = ids(a); const second = ids(b);
|
|
150
173
|
const sorted = [...first].sort((x, y) => (x < y ? -1 : x > y ? 1 : 0));
|
|
@@ -65,9 +65,16 @@ describe('v2-advertised-fixtures-exist (RFC 0168 §C.3)', () => {
|
|
|
65
65
|
const ids = advertisedIds(doc);
|
|
66
66
|
if (ids.length === 0) return softSkip('inapplicable', 'the host advertises no fixtures[]');
|
|
67
67
|
|
|
68
|
-
// Deterministic sample
|
|
68
|
+
// Deterministic sample spread across the sorted list, so a failure is
|
|
69
69
|
// reproducible and a host cannot pass by luck of ordering.
|
|
70
|
-
|
|
70
|
+
// Unfailable-leg audit, 2026-09-26: the sample was the first N by sort
|
|
71
|
+
// order, so an unseeded fixture sorting 6th or later was never attempted.
|
|
72
|
+
// Indices round(i*(n-1)/(SAMPLE-1)), deduped, reach both ends and between.
|
|
73
|
+
const sorted = [...new Set(ids)].sort();
|
|
74
|
+
const picks = sorted.length <= SAMPLE
|
|
75
|
+
? sorted.map((_, i) => i)
|
|
76
|
+
: Array.from({ length: SAMPLE }, (_, i) => Math.round((i * (sorted.length - 1)) / (SAMPLE - 1)));
|
|
77
|
+
const sample = [...new Set(picks)].map((i) => sorted[i]!);
|
|
71
78
|
const unreachable: string[] = [];
|
|
72
79
|
let attempted = 0;
|
|
73
80
|
for (const id of sample) {
|
|
@@ -79,7 +86,14 @@ describe('v2-advertised-fixtures-exist (RFC 0168 §C.3)', () => {
|
|
|
79
86
|
// 201 is the claim honoured. A 4xx that names the workflow as unknown is
|
|
80
87
|
// the drift this leg exists to catch; any other status is a different
|
|
81
88
|
// problem and is not judged here.
|
|
82
|
-
|
|
89
|
+
// The comment above always said only an unknown-workflow refusal is
|
|
90
|
+
// judged, but the code counted ANY non-201. The 2026-09-26 audit's wider
|
|
91
|
+
// sample then hit fixtures that need an Idempotency-Key or specific
|
|
92
|
+
// inputs (a validation 4xx) and failed a conforming host. v2 has no
|
|
93
|
+
// dedicated unknown-workflow code, so the signal is 404, or a 4xx whose
|
|
94
|
+
// details.field names workflowId.
|
|
95
|
+
const field = (res.json as { details?: { field?: unknown } } | null)?.details?.field;
|
|
96
|
+
if (res.status === 404 || (res.status >= 400 && res.status < 500 && field === 'workflowId')) unreachable.push(`${id} → ${res.status}`);
|
|
83
97
|
}
|
|
84
98
|
if (attempted === 0) return softSkip('blocked', 'no advertised fixture could be attempted');
|
|
85
99
|
expect(
|
|
@@ -118,12 +118,18 @@ describe('RFC 0205 §A.3 — a url Part does not resolve beyond the caller\'s au
|
|
|
118
118
|
const tenantB = process.env['OPENWOP_TEST_TENANT_B_API_KEY'];
|
|
119
119
|
if (!tenantB) return softSkip('blocked', 'the artifact carries a url Part but OPENWOP_TEST_TENANT_B_API_KEY is not supplied — the second-tenant leg cannot run');
|
|
120
120
|
const base = loadEnv().baseUrl;
|
|
121
|
+
// unfailable-leg audit, 2026-09-26: the probes use redirect:'manual' and
|
|
122
|
+
// only failed on a 2xx, so a url that answered 302 to a public pre-signed
|
|
123
|
+
// object-store URL — readable by tenant B and by anyone — passed. A 3xx is
|
|
124
|
+
// the host resolving the url for that caller; the only answers that do not
|
|
125
|
+
// resolve it are a refusal (401/403) or a non-disclosing 404.
|
|
126
|
+
const REFUSED = new Set([401, 403, 404]);
|
|
121
127
|
for (const u of urls) {
|
|
122
128
|
const abs = new URL(u, base).toString();
|
|
123
129
|
const asB = await fetch(abs, { headers: { Authorization: `Bearer ${tenantB}` }, redirect: 'manual' });
|
|
124
|
-
expect(
|
|
130
|
+
expect(REFUSED.has(asB.status), req('openwop.requirement.0205.artifact-url-part-scoped', 'RFC 0205 §A.3', `${abs} MUST NOT resolve for tenant B's credential: expected 401/403/404, got ${asB.status}${asB.status >= 300 && asB.status < 400 ? ` (a redirect to ${asB.headers.get('location') ?? 'no Location'} resolves the url)` : ''}`)).toBe(true);
|
|
125
131
|
const anon = await fetch(abs, { redirect: 'manual' });
|
|
126
|
-
expect(
|
|
132
|
+
expect(REFUSED.has(anon.status), req('openwop.requirement.0205.artifact-url-part-scoped', 'RFC 0205 §A.3', `${abs} MUST NOT resolve with no credential: expected 401/403/404, got ${anon.status}${anon.status >= 300 && anon.status < 400 ? ` (a redirect to ${anon.headers.get('location') ?? 'no Location'} resolves the url)` : ''}`)).toBe(true);
|
|
127
133
|
}
|
|
128
134
|
});
|
|
129
135
|
});
|
|
@@ -221,9 +221,14 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
|
|
|
221
221
|
const res = await http(() => driver.get(`/runs/${projectBoundId(runId)}/effects`));
|
|
222
222
|
if (res === null) return softSkip('blocked', 'GET /runs/{runId}/effects unreachable (fetch failed)');
|
|
223
223
|
if (res.status === 404 || res.status === 405) return softSkip('inapplicable', `the host advertises idempotency but serves no effect ledger at /runs/{runId}/effects (${res.status})`);
|
|
224
|
-
expect(res.status, req(PER_KIND, 'security-defaults.md §Layer-2 effect identity', 'GET /runs/{runId}/effects MUST answer 200')).toBe(200);
|
|
225
224
|
const effects = ((res.json as { effects?: Array<Record<string, unknown>> } | null)?.effects ?? []);
|
|
226
|
-
|
|
225
|
+
// Unfailable-leg audit (2026-09-26): the status assert used to precede this
|
|
226
|
+
// empty check, so on every host (the noop fixture records no effects) the
|
|
227
|
+
// leg asserted 200 then soft-skipped — `executed-pass` partial-witness folded
|
|
228
|
+
// into the shared per-kind row with no effectId ever read. The empty 200 now
|
|
229
|
+
// returns before any assertion.
|
|
230
|
+
if (res.status === 200 && effects.length === 0) return softSkip('inapplicable', 'the noop fixture records no effects — no effectId on the wire to read (a host with an effect-producing fixture witnesses this leg)');
|
|
231
|
+
expect(res.status, req(PER_KIND, 'security-defaults.md §Layer-2 effect identity', 'GET /runs/{runId}/effects MUST answer 200')).toBe(200);
|
|
227
232
|
for (const e of effects) expectBound(e['effectId'], 'effectId', 'GET /runs/{runId}/effects', PER_KIND);
|
|
228
233
|
});
|
|
229
234
|
|
|
@@ -244,9 +249,13 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
|
|
|
244
249
|
try {
|
|
245
250
|
const res = await http(() => driver.get(`/webhooks/${projectBoundId(webhookId)}/dead-letters`));
|
|
246
251
|
if (res === null) return softSkip('blocked', 'GET /webhooks/{webhookId}/dead-letters unreachable (fetch failed)');
|
|
247
|
-
expect(res.status, req(PER_KIND, 'RFC 0188 §A.1', 'a host advertising webhooks.deadLetter MUST serve the dead-letter read (200)')).toBe(200);
|
|
248
252
|
const rows = ((res.json as { deliveries?: Array<Record<string, unknown>> } | null)?.deliveries ?? []);
|
|
249
|
-
|
|
253
|
+
// Unfailable-leg audit (2026-09-26): as with effectId above, the 200 assert
|
|
254
|
+
// preceded the empty check, so a fresh subscription (always empty) recorded
|
|
255
|
+
// `executed-pass` partial-witness with no deliveryId read. Empty-200 now
|
|
256
|
+
// returns before any assertion.
|
|
257
|
+
if (res.status === 200 && rows.length === 0) return softSkip('inapplicable', 'the subscription has no dead-lettered delivery in this run — the read is served and the shape is unwitnessed here; v2-webhook-durable-delivery drives an exhaustion and asserts the record');
|
|
258
|
+
expect(res.status, req(PER_KIND, 'RFC 0188 §A.1', 'a host advertising webhooks.deadLetter MUST serve the dead-letter read (200)')).toBe(200);
|
|
250
259
|
for (const r of rows) expectBound(r['deliveryId'], 'deliveryId', 'GET /webhooks/{webhookId}/dead-letters', PER_KIND);
|
|
251
260
|
} finally {
|
|
252
261
|
await http(() => driver.delete(`/webhooks/${projectBoundId(webhookId)}`));
|