@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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow-chain pack expansion — live-host gate (RFC 0013 Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* Capability-gated scenario. Skips when the host doesn't advertise
|
|
5
|
+
* `capabilities.workflowChainPacks.supported: true`. Asserts the host's
|
|
6
|
+
* vendor-prefixed expansion endpoint (`POST /v1/host/sample/workflow-
|
|
7
|
+
* chain:expand` — vendor prefix per `host-extensions.md` §"Canonical
|
|
8
|
+
* prefixes") returns expanded fragments equivalent to the spec-
|
|
9
|
+
* authoritative `expandChain()` reference library.
|
|
10
|
+
*
|
|
11
|
+
* Why this exists: the four server-free chain scenarios
|
|
12
|
+
* (manifest-validation, signature-verification, expansion,
|
|
13
|
+
* unresolvable-typeid) cover the pure logic. This scenario proves a
|
|
14
|
+
* reference host wraps the algorithm correctly — fetch + verify +
|
|
15
|
+
* locate + expand — and emits the same wire shape any consumer
|
|
16
|
+
* implementing the spec would. Without it, the RFC's "reference host
|
|
17
|
+
* implements expansion" acceptance criterion cannot be verified
|
|
18
|
+
* end-to-end against an actual deployment.
|
|
19
|
+
*
|
|
20
|
+
* Coverage:
|
|
21
|
+
* 1. Discovery advertises the capability (precondition for the rest).
|
|
22
|
+
* 2. Positive — 1-node chain expands; substituted config + rewritten
|
|
23
|
+
* id + propagated capabilities match the pure-library output for
|
|
24
|
+
* the same input.
|
|
25
|
+
* 3. Positive — 2-node chain with edges expands; edge endpoints
|
|
26
|
+
* reference the rewritten ids.
|
|
27
|
+
* 4. Negative — unknown packName → 404 `pack_not_found`.
|
|
28
|
+
* 5. Negative — known pack, unknown chainId → 404 `chain_not_found`.
|
|
29
|
+
* 6. Negative — malformed body (no chainId) → 422 `invalid_request`.
|
|
30
|
+
*
|
|
31
|
+
* @see spec/v1/workflow-chain-packs.md §"Expansion semantics (normative)"
|
|
32
|
+
* @see capabilities.md §workflowChainPacks
|
|
33
|
+
* @see RFCS/0013-workflow-chain-packs.md (Phase 3)
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { describe, it, expect } from 'vitest';
|
|
37
|
+
|
|
38
|
+
import { driver } from '../lib/driver.js';
|
|
39
|
+
import { loadEnv } from '../lib/env.js';
|
|
40
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
41
|
+
|
|
42
|
+
const PROFILE = 'workflowChainPacks';
|
|
43
|
+
const SAMPLE_PACK = 'vendor.openwop.workflow-chain-sample';
|
|
44
|
+
const CHAIN_1_NODE = 'vendor.openwop.workflow-chain-sample.summarize-text';
|
|
45
|
+
const CHAIN_2_NODE = 'vendor.openwop.workflow-chain-sample.fetch-and-summarize';
|
|
46
|
+
const EXPAND_PATH = '/v1/host/sample/workflow-chain:expand';
|
|
47
|
+
|
|
48
|
+
interface ChainCaps {
|
|
49
|
+
supported?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function isExpansionAdvertised(): Promise<boolean> {
|
|
53
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
54
|
+
const caps =
|
|
55
|
+
(disco.json as { capabilities?: { workflowChainPacks?: ChainCaps } }).capabilities
|
|
56
|
+
?.workflowChainPacks ?? {};
|
|
57
|
+
return caps.supported === true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('workflow-chain-host-expansion: live host wraps expansion algorithm correctly', () => {
|
|
61
|
+
it('host discovery advertises workflowChainPacks.supported when expansion is implemented', async () => {
|
|
62
|
+
loadEnv();
|
|
63
|
+
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
64
|
+
|
|
65
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
66
|
+
const caps = (disco.json as { capabilities?: { workflowChainPacks?: ChainCaps } }).capabilities
|
|
67
|
+
?.workflowChainPacks;
|
|
68
|
+
expect(
|
|
69
|
+
caps,
|
|
70
|
+
driver.describe(
|
|
71
|
+
'capabilities.md §workflowChainPacks',
|
|
72
|
+
'host advertising the capability MUST set `supported: true` in the discovery block',
|
|
73
|
+
),
|
|
74
|
+
).toBeDefined();
|
|
75
|
+
expect(caps?.supported).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('positive — 1-node chain expansion via the host returns substituted config + rewritten id', async () => {
|
|
79
|
+
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
80
|
+
|
|
81
|
+
const res = await driver.post(EXPAND_PATH, {
|
|
82
|
+
packName: SAMPLE_PACK,
|
|
83
|
+
chainId: CHAIN_1_NODE,
|
|
84
|
+
parameters: {
|
|
85
|
+
sourceText: 'The quick brown fox jumps over the lazy dog.',
|
|
86
|
+
targetLength: 'one-sentence',
|
|
87
|
+
tone: 'casual',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(res.status).toBe(200);
|
|
92
|
+
const body = res.json as {
|
|
93
|
+
expansionId: string;
|
|
94
|
+
chainId: string;
|
|
95
|
+
packName: string;
|
|
96
|
+
packVersion: string;
|
|
97
|
+
nodes: Array<{
|
|
98
|
+
id: string;
|
|
99
|
+
typeId: string;
|
|
100
|
+
config?: { systemPrompt?: string };
|
|
101
|
+
capabilities?: string[];
|
|
102
|
+
}>;
|
|
103
|
+
edges: Array<unknown>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
expect(body.chainId).toBe(CHAIN_1_NODE);
|
|
107
|
+
expect(body.packName).toBe(SAMPLE_PACK);
|
|
108
|
+
expect(body.packVersion).toBe('1.0.0');
|
|
109
|
+
expect(body.nodes).toHaveLength(1);
|
|
110
|
+
expect(body.edges).toHaveLength(0);
|
|
111
|
+
expect(typeof body.expansionId).toBe('string');
|
|
112
|
+
expect(body.expansionId.length).toBeGreaterThan(0);
|
|
113
|
+
|
|
114
|
+
const node = body.nodes[0]!;
|
|
115
|
+
// Step 6: id rewriting — chainId's dots become underscores +
|
|
116
|
+
// expansionId suffix + original fragment id.
|
|
117
|
+
expect(node.id).toMatch(
|
|
118
|
+
/^vendor_openwop_workflow-chain-sample_summarize-text_[a-f0-9]+_summarize-call$/,
|
|
119
|
+
);
|
|
120
|
+
expect(node.typeId).toBe('core.ai.callPrompt');
|
|
121
|
+
|
|
122
|
+
// Step 5: literal substitution.
|
|
123
|
+
const sysPrompt = node.config?.systemPrompt ?? '';
|
|
124
|
+
expect(sysPrompt).toContain('a one-sentence summary');
|
|
125
|
+
expect(sysPrompt).toContain('a casual tone');
|
|
126
|
+
expect(sysPrompt).toContain('The quick brown fox jumps over the lazy dog.');
|
|
127
|
+
|
|
128
|
+
// Step 8: capability propagation.
|
|
129
|
+
expect(node.capabilities).toEqual(['cacheable']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('positive — 2-node chain with edges expands with rewritten edge endpoints', async () => {
|
|
133
|
+
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
134
|
+
|
|
135
|
+
const res = await driver.post(EXPAND_PATH, {
|
|
136
|
+
packName: SAMPLE_PACK,
|
|
137
|
+
chainId: CHAIN_2_NODE,
|
|
138
|
+
parameters: {
|
|
139
|
+
url: 'https://example.com/article',
|
|
140
|
+
targetLength: 'executive-summary',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
expect(res.status).toBe(200);
|
|
144
|
+
|
|
145
|
+
const body = res.json as {
|
|
146
|
+
expansionId: string;
|
|
147
|
+
nodes: Array<{ id: string; typeId: string; capabilities?: string[] }>;
|
|
148
|
+
edges: Array<{ from: string; to: string }>;
|
|
149
|
+
};
|
|
150
|
+
expect(body.nodes).toHaveLength(2);
|
|
151
|
+
expect(body.edges).toHaveLength(1);
|
|
152
|
+
|
|
153
|
+
// Both expanded nodes get the same prefix; the edge's `from`/`to`
|
|
154
|
+
// refer to fragment node ids and so get rewritten with the same
|
|
155
|
+
// prefix (port suffix preserved).
|
|
156
|
+
const edge = body.edges[0]!;
|
|
157
|
+
const prefix = `vendor_openwop_workflow-chain-sample_fetch-and-summarize_${body.expansionId}_`;
|
|
158
|
+
expect(edge.from).toBe(`${prefix}fetch.body`);
|
|
159
|
+
expect(edge.to).toBe(`${prefix}summarize.sourceText`);
|
|
160
|
+
|
|
161
|
+
// side-effectful capability propagated to BOTH expanded nodes.
|
|
162
|
+
for (const node of body.nodes) {
|
|
163
|
+
expect(node.capabilities, `node ${node.id} inherits chain capability`).toEqual(['side-effectful']);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('negative — unknown pack returns 404 pack_not_found', async () => {
|
|
168
|
+
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
169
|
+
|
|
170
|
+
const res = await driver.post(EXPAND_PATH, {
|
|
171
|
+
packName: 'vendor.acme.does-not-exist',
|
|
172
|
+
chainId: 'whatever',
|
|
173
|
+
parameters: {},
|
|
174
|
+
});
|
|
175
|
+
expect(res.status).toBe(404);
|
|
176
|
+
expect((res.json as { error: string }).error).toBe('pack_not_found');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('negative — known pack but unknown chainId returns 404 chain_not_found', async () => {
|
|
180
|
+
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
181
|
+
|
|
182
|
+
const res = await driver.post(EXPAND_PATH, {
|
|
183
|
+
packName: SAMPLE_PACK,
|
|
184
|
+
chainId: 'vendor.openwop.workflow-chain-sample.does-not-exist',
|
|
185
|
+
parameters: {},
|
|
186
|
+
});
|
|
187
|
+
expect(res.status).toBe(404);
|
|
188
|
+
expect((res.json as { error: string }).error).toBe('chain_not_found');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('negative — malformed request body returns 422 invalid_request', async () => {
|
|
192
|
+
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
193
|
+
|
|
194
|
+
// Missing chainId.
|
|
195
|
+
const res = await driver.post(EXPAND_PATH, {
|
|
196
|
+
packName: SAMPLE_PACK,
|
|
197
|
+
parameters: {},
|
|
198
|
+
});
|
|
199
|
+
expect(res.status).toBe(422);
|
|
200
|
+
expect((res.json as { error: string }).error).toBe('invalid_request');
|
|
201
|
+
});
|
|
202
|
+
});
|