@openwop/openwop-conformance 1.2.0 → 1.3.0

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 (53) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -2
  3. package/api/redocly.yaml +15 -0
  4. package/coverage.md +2 -1
  5. package/fixtures/conformance-agent-reasoning-streaming.json +37 -0
  6. package/fixtures/conformance-dispatch-cancellable-child.json +27 -0
  7. package/fixtures/conformance-dispatch-deterministic-fail-child.json +30 -0
  8. package/fixtures/conformance-dispatch-input-mapping-no-default.json +49 -0
  9. package/fixtures/conformance-dispatch-per-worker-override.json +59 -0
  10. package/fixtures/conformance-subworkflow-input-mapping-no-default.json +33 -0
  11. package/fixtures.md +6 -0
  12. package/package.json +1 -1
  13. package/schemas/capabilities.schema.json +16 -0
  14. package/schemas/core-conformance-mock-agent-config.schema.json +5 -0
  15. package/schemas/run-event-payloads.schema.json +35 -1
  16. package/schemas/run-event.schema.json +2 -0
  17. package/src/lib/driver.ts +15 -0
  18. package/src/lib/env.ts +51 -0
  19. package/src/lib/event-log-query.ts +62 -0
  20. package/src/lib/fixtures.ts +38 -1
  21. package/src/lib/host-toggle.ts +54 -0
  22. package/src/lib/multi-agent-capabilities.ts +10 -0
  23. package/src/lib/otel-scrape.ts +59 -0
  24. package/src/scenarios/agentReasoningStreaming.test.ts +193 -0
  25. package/src/scenarios/aiEnvelope.capBreached.test.ts +97 -9
  26. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +128 -10
  27. package/src/scenarios/aiEnvelope.correlationReplay.test.ts +236 -21
  28. package/src/scenarios/aiEnvelope.redaction.test.ts +204 -24
  29. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +158 -19
  30. package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +59 -8
  31. package/src/scenarios/aiEnvelope.universalKinds.test.ts +100 -9
  32. package/src/scenarios/blob-presign-expiry.test.ts +35 -2
  33. package/src/scenarios/blob-roundtrip.test.ts +0 -0
  34. package/src/scenarios/cache-ttl-expiry.test.ts +28 -2
  35. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +34 -3
  36. package/src/scenarios/dispatch-input-mapping.test.ts +75 -6
  37. package/src/scenarios/dispatch-output-mapping.test.ts +96 -6
  38. package/src/scenarios/fixtures-gating.test.ts +139 -1
  39. package/src/scenarios/kv-ttl-expiry.test.ts +33 -2
  40. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +19 -0
  41. package/src/scenarios/pack-registry-publish.test.ts +231 -51
  42. package/src/scenarios/provider-usage.test.ts +185 -0
  43. package/src/scenarios/queue-ack-nack-dlq.test.ts +57 -3
  44. package/src/scenarios/queue-publish-consume-roundtrip.test.ts +43 -3
  45. package/src/scenarios/replay-llm-cache-key.test.ts +166 -25
  46. package/src/scenarios/search-bm25-roundtrip.test.ts +47 -2
  47. package/src/scenarios/sql-transaction-atomicity.test.ts +31 -2
  48. package/src/scenarios/stream-subscribe-from-beginning.test.ts +39 -2
  49. package/src/scenarios/subworkflow-input-mapping.test.ts +77 -7
  50. package/src/scenarios/table-cursor-pagination.test.ts +40 -2
  51. package/src/scenarios/table-schema-enforcement.test.ts +39 -2
  52. package/src/scenarios/vector-knn-roundtrip.test.ts +43 -3
  53. package/src/scenarios/workflow-chain-host-expansion.test.ts +202 -0
@@ -55,11 +55,101 @@ describe.skipIf(SKIP)('dispatch-output-mapping: child → parent variable harves
55
55
  )).toBe('done');
56
56
  });
57
57
 
58
- it.todo(
59
- 'HVMAP-1b-failed: child terminates with `failed` status; outputMapping MUST be skipped; parent variables stay at pre-dispatch state for that child. Requires a child fixture that fails deterministically.',
60
- );
58
+ });
59
+
60
+ interface RunEvent { readonly type: string; readonly nodeId?: string; readonly payload?: { childRunId?: string; childWorkflowId?: string } & Record<string, unknown>; }
61
+
62
+ /** Register a parent workflow that dispatches to a specific child fixture
63
+ * with outputMapping. Returns the registered parent's workflowId.
64
+ * The parent declares `parentResult` with a sentinel default so the
65
+ * test can verify it stayed at the sentinel (NOT overwritten by
66
+ * outputMapping) when the child terminates non-completed. */
67
+ async function registerParent(childFixtureId: string): Promise<string | null> {
68
+ const workflowId = `hvmap-1b-${childFixtureId.replace(/[^a-zA-Z0-9_.-]/g, '-')}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
69
+ const def = {
70
+ workflowId,
71
+ nodes: [
72
+ {
73
+ nodeId: 'supervisor',
74
+ typeId: 'core.orchestrator.supervisor',
75
+ config: {
76
+ mockDispatchPlan: [
77
+ { kind: 'next-worker', nextWorkerIds: [childFixtureId] },
78
+ { kind: 'terminate', reason: 'goal-reached' },
79
+ ],
80
+ },
81
+ },
82
+ {
83
+ nodeId: 'dispatch',
84
+ typeId: 'core.dispatch',
85
+ config: {
86
+ askUserRouting: 'auto',
87
+ workerDispatchModel: 'child-run',
88
+ fanOutPolicy: 'sequential',
89
+ outputMapping: { parentResult: 'childOutcome' },
90
+ },
91
+ },
92
+ ],
93
+ };
94
+ const res = await driver.post('/v1/host/sample/workflows', def);
95
+ if (res.status !== 201) return null;
96
+ return workflowId;
97
+ }
98
+
99
+ describe.skipIf(!isFixtureAdvertised('conformance-dispatch-deterministic-fail-child'))('dispatch-output-mapping: HVMAP-1b-failed (RFC 0022 §B)', () => {
100
+ it('child terminates `failed` → outputMapping MUST be skipped; parent.parentResult stays at sentinel', async () => {
101
+ const parentId = await registerParent('conformance-dispatch-deterministic-fail-child');
102
+ if (!parentId) return; // workflow-register seam not exposed — soft-skip
103
+ const create = await driver.post('/v1/runs', { workflowId: parentId });
104
+ expect(create.status).toBe(201);
105
+ const parentRunId = (create.json as { runId: string }).runId;
106
+ const terminal = (await pollUntilTerminal(parentRunId)) as RunSnapshot & { variables?: Record<string, unknown> };
107
+ // Parent reaches some terminal state (completed if it tolerates failed children + supervisor terminates; failed if not).
108
+ // Either way, parentResult MUST NOT be overwritten with the child's "this-should-not-be-harvested" sentinel.
109
+ const parentVars = terminal.variables ?? {};
110
+ expect(
111
+ parentVars.parentResult,
112
+ driver.describe(
113
+ 'RFCS/0022-dispatch-input-output-mapping.md §B',
114
+ 'outputMapping MUST be SKIPPED when child terminates failed; parent variable MUST NOT be overwritten',
115
+ ),
116
+ ).not.toBe('this-should-not-be-harvested');
117
+ });
118
+ });
61
119
 
62
- it.todo(
63
- 'HVMAP-1b-cancelled: child terminates with `cancelled` status; outputMapping MUST be skipped; parent variables stay at pre-dispatch state for that child. Requires a child fixture that supports external cancellation.',
64
- );
120
+ describe.skipIf(!isFixtureAdvertised('conformance-dispatch-cancellable-child'))('dispatch-output-mapping: HVMAP-1b-cancelled (RFC 0022 §B)', () => {
121
+ it('child terminates `cancelled` outputMapping MUST be skipped; parent.parentResult stays at sentinel', async () => {
122
+ const parentId = await registerParent('conformance-dispatch-cancellable-child');
123
+ if (!parentId) return; // soft-skip
124
+ const create = await driver.post('/v1/runs', { workflowId: parentId });
125
+ expect(create.status).toBe(201);
126
+ const parentRunId = (create.json as { runId: string }).runId;
127
+
128
+ // Poll for the node.dispatched event so we can cancel the child mid-flight.
129
+ const start = Date.now();
130
+ let childRunId: string | undefined;
131
+ while (Date.now() - start < 10_000) {
132
+ const eventsRes = await driver.get(`/v1/runs/${encodeURIComponent(parentRunId)}/events`);
133
+ const events = ((eventsRes.json as { events?: RunEvent[] } | undefined)?.events ?? []);
134
+ const dispatched = events.find((e) => e.type === 'node.dispatched' && e.payload?.childWorkflowId === 'conformance-dispatch-cancellable-child');
135
+ if (dispatched?.payload?.childRunId) {
136
+ childRunId = dispatched.payload.childRunId;
137
+ break;
138
+ }
139
+ await new Promise((r) => setTimeout(r, 250));
140
+ }
141
+ if (!childRunId) return; // dispatch didn't surface child run id — soft-skip
142
+ const cancelRes = await driver.post(`/v1/runs/${encodeURIComponent(childRunId)}/cancel`, { reason: 'hvmap-1b-cancelled test' });
143
+ expect(cancelRes.status === 200 || cancelRes.status === 202).toBe(true);
144
+
145
+ const terminal = (await pollUntilTerminal(parentRunId)) as RunSnapshot & { variables?: Record<string, unknown> };
146
+ const parentVars = terminal.variables ?? {};
147
+ expect(
148
+ parentVars.parentResult,
149
+ driver.describe(
150
+ 'RFCS/0022-dispatch-input-output-mapping.md §B',
151
+ 'outputMapping MUST be SKIPPED when child terminates cancelled; parent variable MUST NOT be overwritten',
152
+ ),
153
+ ).not.toBe('this-should-not-be-harvested');
154
+ });
65
155
  });
@@ -18,7 +18,7 @@
18
18
  * @see RFCS/0003-fixture-gating.md
19
19
  */
20
20
 
21
- import { describe, it, expect, beforeEach } from 'vitest';
21
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
22
22
  import {
23
23
  isFixtureAdvertised,
24
24
  setAdvertisedFixtures,
@@ -26,6 +26,7 @@ import {
26
26
  isFixtureCacheReady,
27
27
  __resetForTests,
28
28
  } from '../lib/fixtures.js';
29
+ import { isScenarioOptedOut } from '../lib/env.js';
29
30
 
30
31
  beforeEach(() => {
31
32
  __resetForTests();
@@ -135,3 +136,140 @@ describe('fixtures: __resetForTests', () => {
135
136
  expect(getAdvertisedFixtures()).toBe(null);
136
137
  });
137
138
  });
139
+
140
+ describe('fixtures: OPENWOP_OPTED_OUT_FIXTURES env filtering', () => {
141
+ // The opt-out predicate is re-read inside setAdvertisedFixtures() on
142
+ // every call, so mutating process.env between cases (and re-calling
143
+ // setAdvertisedFixtures) re-evaluates the parse. afterEach restores
144
+ // the original env so other suites aren't affected.
145
+ const ORIGINAL = process.env.OPENWOP_OPTED_OUT_FIXTURES;
146
+ afterEach(() => {
147
+ if (ORIGINAL === undefined) delete process.env.OPENWOP_OPTED_OUT_FIXTURES;
148
+ else process.env.OPENWOP_OPTED_OUT_FIXTURES = ORIGINAL;
149
+ });
150
+
151
+ it('exact id is filtered out of the advertised set', () => {
152
+ process.env.OPENWOP_OPTED_OUT_FIXTURES = 'conformance-dispatch-input-mapping';
153
+ setAdvertisedFixtures({
154
+ fixtures: ['conformance-noop', 'conformance-dispatch-input-mapping'],
155
+ });
156
+ expect(isFixtureAdvertised('conformance-noop')).toBe(true);
157
+ expect(isFixtureAdvertised('conformance-dispatch-input-mapping')).toBe(false);
158
+ });
159
+
160
+ it('trailing-* glob filters every matching id', () => {
161
+ process.env.OPENWOP_OPTED_OUT_FIXTURES = 'conformance-dispatch-*';
162
+ setAdvertisedFixtures({
163
+ fixtures: [
164
+ 'conformance-noop',
165
+ 'conformance-dispatch-input-mapping',
166
+ 'conformance-dispatch-output-mapping',
167
+ 'conformance-dispatch-cross-worker-handoff',
168
+ ],
169
+ });
170
+ expect(isFixtureAdvertised('conformance-noop')).toBe(true);
171
+ expect(isFixtureAdvertised('conformance-dispatch-input-mapping')).toBe(false);
172
+ expect(isFixtureAdvertised('conformance-dispatch-output-mapping')).toBe(false);
173
+ expect(isFixtureAdvertised('conformance-dispatch-cross-worker-handoff')).toBe(false);
174
+ });
175
+
176
+ it('exact + glob entries mix in one env value', () => {
177
+ process.env.OPENWOP_OPTED_OUT_FIXTURES =
178
+ 'conformance-dispatch-*,conformance-subworkflow-input-mapping';
179
+ setAdvertisedFixtures({
180
+ fixtures: [
181
+ 'conformance-noop',
182
+ 'conformance-dispatch-input-mapping',
183
+ 'conformance-subworkflow-input-mapping',
184
+ 'conformance-subworkflow-parent',
185
+ ],
186
+ });
187
+ expect(isFixtureAdvertised('conformance-noop')).toBe(true);
188
+ expect(isFixtureAdvertised('conformance-dispatch-input-mapping')).toBe(false);
189
+ expect(isFixtureAdvertised('conformance-subworkflow-input-mapping')).toBe(false);
190
+ // subworkflow-parent is NOT subworkflow-input-mapping — exact match required.
191
+ expect(isFixtureAdvertised('conformance-subworkflow-parent')).toBe(true);
192
+ });
193
+
194
+ it('non-matching opt-out entries leave the advertised set intact', () => {
195
+ process.env.OPENWOP_OPTED_OUT_FIXTURES = 'conformance-nonexistent';
196
+ setAdvertisedFixtures({ fixtures: ['conformance-noop'] });
197
+ expect(isFixtureAdvertised('conformance-noop')).toBe(true);
198
+ expect(getAdvertisedFixtures()?.size).toBe(1);
199
+ });
200
+
201
+ it('empty / whitespace-only entries are ignored', () => {
202
+ process.env.OPENWOP_OPTED_OUT_FIXTURES = ', ,conformance-noop, ,';
203
+ setAdvertisedFixtures({ fixtures: ['conformance-noop', 'conformance-delay'] });
204
+ expect(isFixtureAdvertised('conformance-noop')).toBe(false);
205
+ expect(isFixtureAdvertised('conformance-delay')).toBe(true);
206
+ });
207
+
208
+ it('unset env behaves identically to no filtering', () => {
209
+ delete process.env.OPENWOP_OPTED_OUT_FIXTURES;
210
+ setAdvertisedFixtures({ fixtures: ['conformance-noop', 'conformance-delay'] });
211
+ expect(getAdvertisedFixtures()?.size).toBe(2);
212
+ });
213
+
214
+ it('whitespace-only env behaves identically to unset', () => {
215
+ process.env.OPENWOP_OPTED_OUT_FIXTURES = ' ';
216
+ setAdvertisedFixtures({ fixtures: ['conformance-noop'] });
217
+ expect(isFixtureAdvertised('conformance-noop')).toBe(true);
218
+ });
219
+
220
+ it('env is re-read on each setAdvertisedFixtures call (no memoization)', () => {
221
+ process.env.OPENWOP_OPTED_OUT_FIXTURES = 'conformance-noop';
222
+ setAdvertisedFixtures({ fixtures: ['conformance-noop', 'conformance-delay'] });
223
+ expect(isFixtureAdvertised('conformance-noop')).toBe(false);
224
+
225
+ // Mutate env and re-set — the new env value MUST take effect.
226
+ process.env.OPENWOP_OPTED_OUT_FIXTURES = 'conformance-delay';
227
+ setAdvertisedFixtures({ fixtures: ['conformance-noop', 'conformance-delay'] });
228
+ expect(isFixtureAdvertised('conformance-noop')).toBe(true);
229
+ expect(isFixtureAdvertised('conformance-delay')).toBe(false);
230
+ });
231
+ });
232
+
233
+ describe('env: OPENWOP_OPTED_OUT_SCENARIOS predicate', () => {
234
+ const ORIGINAL = process.env.OPENWOP_OPTED_OUT_SCENARIOS;
235
+ afterEach(() => {
236
+ if (ORIGINAL === undefined) delete process.env.OPENWOP_OPTED_OUT_SCENARIOS;
237
+ else process.env.OPENWOP_OPTED_OUT_SCENARIOS = ORIGINAL;
238
+ });
239
+
240
+ it('unset env → every scenario id returns false', () => {
241
+ delete process.env.OPENWOP_OPTED_OUT_SCENARIOS;
242
+ expect(isScenarioOptedOut('otel-trace-propagation-subworkflow')).toBe(false);
243
+ expect(isScenarioOptedOut('any-scenario')).toBe(false);
244
+ });
245
+
246
+ it('exact scenario id match returns true', () => {
247
+ process.env.OPENWOP_OPTED_OUT_SCENARIOS = 'otel-trace-propagation-subworkflow';
248
+ expect(isScenarioOptedOut('otel-trace-propagation-subworkflow')).toBe(true);
249
+ expect(isScenarioOptedOut('otel-trace-propagation')).toBe(false);
250
+ });
251
+
252
+ it('CSV with multiple ids matches each entry exactly', () => {
253
+ process.env.OPENWOP_OPTED_OUT_SCENARIOS = 'scenario-a,scenario-b,scenario-c';
254
+ expect(isScenarioOptedOut('scenario-a')).toBe(true);
255
+ expect(isScenarioOptedOut('scenario-b')).toBe(true);
256
+ expect(isScenarioOptedOut('scenario-c')).toBe(true);
257
+ expect(isScenarioOptedOut('scenario-d')).toBe(false);
258
+ });
259
+
260
+ it('whitespace around entries is tolerated', () => {
261
+ process.env.OPENWOP_OPTED_OUT_SCENARIOS = ' scenario-a , scenario-b ';
262
+ expect(isScenarioOptedOut('scenario-a')).toBe(true);
263
+ expect(isScenarioOptedOut('scenario-b')).toBe(true);
264
+ });
265
+
266
+ it('env is re-read on each call (no memoization)', () => {
267
+ process.env.OPENWOP_OPTED_OUT_SCENARIOS = 'scenario-a';
268
+ expect(isScenarioOptedOut('scenario-a')).toBe(true);
269
+ expect(isScenarioOptedOut('scenario-b')).toBe(false);
270
+
271
+ process.env.OPENWOP_OPTED_OUT_SCENARIOS = 'scenario-b';
272
+ expect(isScenarioOptedOut('scenario-a')).toBe(false);
273
+ expect(isScenarioOptedOut('scenario-b')).toBe(true);
274
+ });
275
+ });
@@ -42,6 +42,37 @@ describe('kv-ttl-expiry: advertisement shape (RFC 0015)', () => {
42
42
  });
43
43
  });
44
44
 
45
- describe('kv-ttl-expiry: behavioral assertions (placeholders need host test seam)', () => {
46
- it.todo("set with ttl=2 get at t+1 returns the value; get at t+3 returns not-found");
45
+ async function call(op: string, args: Record<string, unknown>) {
46
+ return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'kv', op, args });
47
+ }
48
+
49
+ describe('kv-ttl-expiry: behavioral (RFC 0015 §B point 3 — 1s TTL drift)', () => {
50
+ it('set with ttlSeconds=2 → get before expiry returns value; get after expiry returns found:false', async () => {
51
+ const probe = await call('get', { key: '__ttl-probe__' });
52
+ if (probe.status === 404) return; // host doesn't expose the seam
53
+ const key = `ttl-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
54
+ const setRes = await call('set', { key, value: 'expires-soon', ttlSeconds: 2 });
55
+ expect(setRes.status).toBe(200);
56
+
57
+ // Read within the window
58
+ const within = await call('get', { key });
59
+ expect(within.status).toBe(200);
60
+ const withinBody = within.json as { value?: unknown; found?: boolean };
61
+ expect(
62
+ withinBody.value,
63
+ driver.describe('RFC 0015 §B point 3', 'get within TTL window MUST return the stored value'),
64
+ ).toBe('expires-soon');
65
+ expect(withinBody.found).toBe(true);
66
+
67
+ // Wait past expiry (2s TTL + 1s drift allowance per RFC 0015 §B point 3)
68
+ await new Promise((r) => setTimeout(r, 3000));
69
+
70
+ const after = await call('get', { key });
71
+ expect(after.status).toBe(200);
72
+ const afterBody = after.json as { value?: unknown; found?: boolean };
73
+ expect(
74
+ afterBody.found,
75
+ driver.describe('RFC 0015 §B point 3', 'get after TTL expiry MUST surface as found:false (≤1s drift)'),
76
+ ).toBe(false);
77
+ });
47
78
  });
@@ -23,6 +23,10 @@
23
23
  * - Host doesn't advertise `capabilities.observability`.
24
24
  * - `conformance-subworkflow-parent` fixture not advertised (host
25
25
  * doesn't implement `core.subWorkflow`).
26
+ * - `OPENWOP_OPTED_OUT_SCENARIOS` contains
27
+ * `otel-trace-propagation-subworkflow` — host claims
28
+ * observability + subWorkflow but explicitly does NOT propagate
29
+ * traceparent across the dispatch boundary.
26
30
  *
27
31
  * @see spec/v1/observability.md §"Trace context propagation"
28
32
  * @see spec/v1/node-packs.md §`core.subWorkflow`
@@ -33,9 +37,11 @@ import { describe, it, expect } from 'vitest';
33
37
  import { driver } from '../lib/driver.js';
34
38
  import { pollUntilTerminal } from '../lib/polling.js';
35
39
  import { isFixtureAdvertised } from '../lib/fixtures.js';
40
+ import { isScenarioOptedOut } from '../lib/env.js';
36
41
  import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
37
42
 
38
43
  const PARENT_FIXTURE = 'conformance-subworkflow-parent';
44
+ const SCENARIO_ID = 'otel-trace-propagation-subworkflow';
39
45
 
40
46
  interface RunEvent {
41
47
  type: string;
@@ -64,6 +70,19 @@ async function isObservabilityAdvertised(): Promise<boolean> {
64
70
 
65
71
  describe('otel-trace-propagation-subworkflow: traceparent threads parent → child via core.subWorkflow', () => {
66
72
  it('child run spans inherit the parent run\'s inbound traceId', async () => {
73
+ if (isScenarioOptedOut(SCENARIO_ID)) {
74
+ // Host operator has declared this scenario opted-out via
75
+ // `OPENWOP_OPTED_OUT_SCENARIOS`. Used when the host advertises
76
+ // `conformance-subworkflow-parent` (correctly — non-OTel
77
+ // subworkflow scenarios pass) AND observability (for audit-log
78
+ // integrity), but doesn't propagate traceparent across the
79
+ // `core.subWorkflow` dispatch boundary. Fixture-opt-out would
80
+ // be too coarse (kills passing non-OTel subworkflow tests);
81
+ // capability-opt-out would lie about observability claims.
82
+ // eslint-disable-next-line no-console
83
+ console.warn(`[${SCENARIO_ID}] scenario opted out via OPENWOP_OPTED_OUT_SCENARIOS; skipping`);
84
+ return;
85
+ }
67
86
  if (!getCollector()) {
68
87
  // eslint-disable-next-line no-console
69
88
  console.warn('[otel-trace-propagation-subworkflow] collector not started; skipping');