@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.
- package/CHANGELOG.md +65 -0
- package/README.md +2 -2
- package/api/redocly.yaml +15 -0
- package/coverage.md +2 -1
- package/fixtures/conformance-agent-reasoning-streaming.json +37 -0
- package/fixtures/conformance-dispatch-cancellable-child.json +27 -0
- package/fixtures/conformance-dispatch-deterministic-fail-child.json +30 -0
- package/fixtures/conformance-dispatch-input-mapping-no-default.json +49 -0
- package/fixtures/conformance-dispatch-per-worker-override.json +59 -0
- package/fixtures/conformance-subworkflow-input-mapping-no-default.json +33 -0
- package/fixtures.md +6 -0
- package/package.json +1 -1
- package/schemas/capabilities.schema.json +16 -0
- package/schemas/core-conformance-mock-agent-config.schema.json +5 -0
- package/schemas/run-event-payloads.schema.json +35 -1
- package/schemas/run-event.schema.json +2 -0
- package/src/lib/driver.ts +15 -0
- package/src/lib/env.ts +51 -0
- package/src/lib/event-log-query.ts +62 -0
- package/src/lib/fixtures.ts +38 -1
- package/src/lib/host-toggle.ts +54 -0
- package/src/lib/multi-agent-capabilities.ts +10 -0
- package/src/lib/otel-scrape.ts +59 -0
- package/src/scenarios/agentReasoningStreaming.test.ts +193 -0
- package/src/scenarios/aiEnvelope.capBreached.test.ts +97 -9
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +128 -10
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +236 -21
- package/src/scenarios/aiEnvelope.redaction.test.ts +204 -24
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +158 -19
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +59 -8
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +100 -9
- package/src/scenarios/blob-presign-expiry.test.ts +35 -2
- package/src/scenarios/blob-roundtrip.test.ts +0 -0
- package/src/scenarios/cache-ttl-expiry.test.ts +28 -2
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +34 -3
- package/src/scenarios/dispatch-input-mapping.test.ts +75 -6
- package/src/scenarios/dispatch-output-mapping.test.ts +96 -6
- package/src/scenarios/fixtures-gating.test.ts +139 -1
- package/src/scenarios/kv-ttl-expiry.test.ts +33 -2
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +19 -0
- package/src/scenarios/pack-registry-publish.test.ts +231 -51
- package/src/scenarios/provider-usage.test.ts +185 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +57 -3
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +43 -3
- package/src/scenarios/replay-llm-cache-key.test.ts +166 -25
- package/src/scenarios/search-bm25-roundtrip.test.ts +47 -2
- package/src/scenarios/sql-transaction-atomicity.test.ts +31 -2
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +39 -2
- package/src/scenarios/subworkflow-input-mapping.test.ts +77 -7
- package/src/scenarios/table-cursor-pagination.test.ts +40 -2
- package/src/scenarios/table-schema-enforcement.test.ts +39 -2
- package/src/scenarios/vector-knn-roundtrip.test.ts +43 -3
- 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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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');
|