@openwop/openwop-conformance 1.51.0 → 1.53.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 +10 -0
- package/README.md +2 -2
- package/coverage.md +3 -1
- package/fixtures/pack-manifests/workflow-chain-sample.pack.json +129 -0
- package/fixtures/trigger-events/trigger-event-change.json +17 -0
- package/fixtures/trigger-events/trigger-event-stream.json +16 -0
- package/fixtures.md +7 -4
- package/package.json +1 -1
- package/schemas/capabilities.schema.json +19 -3
- package/schemas/run-event-payloads.schema.json +1 -1
- package/schemas/trigger-event.schema.json +80 -7
- package/schemas/trigger-subscription-registration.schema.json +1 -1
- package/schemas/trigger-subscription.schema.json +1 -1
- package/schemas/workflow-definition.schema.json +1 -1
- package/src/lib/workflow-chain-expansion.ts +23 -7
- package/src/scenarios/fixtures-valid.test.ts +16 -5
- package/src/scenarios/purpose-propagation.test.ts +200 -0
- package/src/scenarios/trigger-stream-cdc-sources.test.ts +215 -0
- package/src/scenarios/workflow-chain-expansion.test.ts +112 -0
- package/src/scenarios/workflow-chain-host-expansion.test.ts +111 -102
|
@@ -6,62 +6,76 @@
|
|
|
6
6
|
* true` — NOT on the semantic `workflowChainPacks.supported` claim, which is
|
|
7
7
|
* witnessed server-free by `workflow-chain-expansion.test.ts`. This decouples a
|
|
8
8
|
* host's honest `supported` / RFC 0124 `deferredParameters` advertisement from
|
|
9
|
-
* this scenario's `vendor.openwop.workflow-chain-sample` fixture
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* this scenario's `vendor.openwop.workflow-chain-sample` fixture.
|
|
10
|
+
*
|
|
11
|
+
* **A-lite follow-up (2026-07-05):** the `vendor.openwop.workflow-chain-sample`
|
|
12
|
+
* pack is now **bundled into the conformance package** at
|
|
13
|
+
* `fixtures/pack-manifests/workflow-chain-sample.pack.json` (host-syncable), and
|
|
14
|
+
* this scenario **LOADS it + derives the expected expansion from the
|
|
15
|
+
* spec-authoritative reference library** (`expandChain()`) instead of hardcoding
|
|
16
|
+
* expected strings — so the published pack is the single source of truth and the
|
|
17
|
+
* assertions can't drift from it. A serving host resolves the SAME bundled pack
|
|
18
|
+
* and MUST produce the SAME expansion the reference library computes.
|
|
19
|
+
*
|
|
12
20
|
* Asserts the host's vendor-prefixed expansion endpoint (`POST /v1/host/sample/
|
|
13
21
|
* workflow-chain:expand` — vendor prefix per `host-extensions.md` §"Canonical
|
|
14
|
-
* prefixes") returns expanded fragments equivalent to the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Why this exists: the four server-free chain scenarios
|
|
18
|
-
* (manifest-validation, signature-verification, expansion,
|
|
19
|
-
* unresolvable-typeid) cover the pure logic. This scenario proves a
|
|
20
|
-
* reference host wraps the algorithm correctly — fetch + verify +
|
|
21
|
-
* locate + expand — and emits the same wire shape any consumer
|
|
22
|
-
* implementing the spec would. Without it, the RFC's "reference host
|
|
23
|
-
* implements expansion" acceptance criterion cannot be verified
|
|
24
|
-
* end-to-end against an actual deployment.
|
|
22
|
+
* prefixes") returns expanded fragments equivalent to `expandChain()` for the
|
|
23
|
+
* same pack + parameters + host-chosen `expansionId`.
|
|
25
24
|
*
|
|
26
25
|
* Coverage:
|
|
27
|
-
* 1. Discovery advertises
|
|
28
|
-
* 2. Positive — 1-node chain expands;
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* reference the rewritten ids.
|
|
26
|
+
* 1. Discovery advertises `hostExpansionSeam` (precondition for the rest).
|
|
27
|
+
* 2. Positive — 1-node chain expands; host output == reference expansion
|
|
28
|
+
* (substituted config + rewritten id + propagated `cacheable`).
|
|
29
|
+
* 3. Positive — 2-node chain with edges expands; host output == reference
|
|
30
|
+
* expansion (rewritten edge endpoints + propagated `side-effectful`).
|
|
33
31
|
* 4. Negative — unknown packName → 404 `pack_not_found`.
|
|
34
32
|
* 5. Negative — known pack, unknown chainId → 404 `chain_not_found`.
|
|
35
33
|
* 6. Negative — malformed body (no chainId) → 422 `invalid_request`.
|
|
36
34
|
*
|
|
37
35
|
* @see spec/v1/workflow-chain-packs.md §"Expansion semantics (normative)"
|
|
38
|
-
* @see
|
|
36
|
+
* @see conformance/src/lib/workflow-chain-expansion.ts (the reference library)
|
|
39
37
|
* @see RFCS/0013-workflow-chain-packs.md (Phase 3)
|
|
40
38
|
*/
|
|
41
39
|
|
|
42
40
|
import { describe, it, expect } from 'vitest';
|
|
41
|
+
import { readFileSync } from 'node:fs';
|
|
42
|
+
import { join } from 'node:path';
|
|
43
43
|
|
|
44
44
|
import { driver } from '../lib/driver.js';
|
|
45
45
|
import { loadEnv } from '../lib/env.js';
|
|
46
46
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
47
|
+
import { FIXTURES_DIR } from '../lib/paths.js';
|
|
48
|
+
import { expandChain, type WorkflowChain } from '../lib/workflow-chain-expansion.js';
|
|
47
49
|
|
|
48
50
|
const PROFILE = 'workflowChainPacks.hostExpansionSeam';
|
|
49
|
-
const
|
|
51
|
+
const EXPAND_PATH = '/v1/host/sample/workflow-chain:expand';
|
|
52
|
+
|
|
53
|
+
// The pack fixture is bundled with the conformance package (ships in `files`),
|
|
54
|
+
// so a host can sync the IDENTICAL pack and this scenario loads it as the
|
|
55
|
+
// contract source of truth (no hardcoded expansion).
|
|
56
|
+
interface SamplePack {
|
|
57
|
+
name: string;
|
|
58
|
+
version: string;
|
|
59
|
+
chains: Array<WorkflowChain>;
|
|
60
|
+
}
|
|
61
|
+
const PACK = JSON.parse(
|
|
62
|
+
readFileSync(join(FIXTURES_DIR, 'pack-manifests', 'workflow-chain-sample.pack.json'), 'utf8'),
|
|
63
|
+
) as SamplePack;
|
|
64
|
+
const SAMPLE_PACK = PACK.name; // vendor.openwop.workflow-chain-sample
|
|
50
65
|
const CHAIN_1_NODE = 'vendor.openwop.workflow-chain-sample.summarize-text';
|
|
51
66
|
const CHAIN_2_NODE = 'vendor.openwop.workflow-chain-sample.fetch-and-summarize';
|
|
52
|
-
|
|
67
|
+
|
|
68
|
+
function chainById(chainId: string): WorkflowChain {
|
|
69
|
+
const c = PACK.chains.find((x) => x.chainId === chainId);
|
|
70
|
+
if (!c) throw new Error(`fixture missing chain ${chainId}`);
|
|
71
|
+
return c;
|
|
72
|
+
}
|
|
53
73
|
|
|
54
74
|
interface ChainCaps {
|
|
55
75
|
supported?: boolean;
|
|
56
76
|
hostExpansionSeam?: boolean;
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
// RFC 0013 erratum (2026-07-05): this live-host scenario witnesses the OPTIONAL
|
|
60
|
-
// `POST /v1/host/sample/workflow-chain:expand` TEST SEAM, gated on its own
|
|
61
|
-
// `workflowChainPacks.hostExpansionSeam` sub-flag — NOT on the semantic
|
|
62
|
-
// `workflowChainPacks.supported` claim (which is witnessed server-free by
|
|
63
|
-
// `workflow-chain-expansion.test.ts`). This decouples the RFC 0124
|
|
64
|
-
// `deferredParameters` flip from the unpublished RFC 0013 sample-pack fixture.
|
|
65
79
|
async function isExpansionAdvertised(): Promise<boolean> {
|
|
66
80
|
const disco = await driver.get('/.well-known/openwop');
|
|
67
81
|
const caps =
|
|
@@ -70,6 +84,15 @@ async function isExpansionAdvertised(): Promise<boolean> {
|
|
|
70
84
|
return caps.hostExpansionSeam === true;
|
|
71
85
|
}
|
|
72
86
|
|
|
87
|
+
interface ExpandResponse {
|
|
88
|
+
expansionId: string;
|
|
89
|
+
chainId: string;
|
|
90
|
+
packName: string;
|
|
91
|
+
packVersion: string;
|
|
92
|
+
nodes: Array<{ id: string; typeId: string; config?: Record<string, unknown>; capabilities?: string[] }>;
|
|
93
|
+
edges: Array<{ from: string; to: string }>;
|
|
94
|
+
}
|
|
95
|
+
|
|
73
96
|
describe('workflow-chain-host-expansion: live host wraps expansion algorithm correctly', () => {
|
|
74
97
|
it('host discovery advertises workflowChainPacks.hostExpansionSeam when the expand seam is served', async () => {
|
|
75
98
|
loadEnv();
|
|
@@ -89,93 +112,82 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
|
|
|
89
112
|
expect(caps?.supported).toBe(true);
|
|
90
113
|
});
|
|
91
114
|
|
|
92
|
-
it('positive — 1-node chain expansion
|
|
115
|
+
it('positive — 1-node chain expansion matches the reference library for the bundled pack', async () => {
|
|
93
116
|
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
94
117
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
sourceText: 'The quick brown fox jumps over the lazy dog.',
|
|
100
|
-
targetLength: 'one-sentence',
|
|
101
|
-
tone: 'casual',
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
expect(res.status).toBe(200);
|
|
106
|
-
const body = res.json as {
|
|
107
|
-
expansionId: string;
|
|
108
|
-
chainId: string;
|
|
109
|
-
packName: string;
|
|
110
|
-
packVersion: string;
|
|
111
|
-
nodes: Array<{
|
|
112
|
-
id: string;
|
|
113
|
-
typeId: string;
|
|
114
|
-
config?: { systemPrompt?: string };
|
|
115
|
-
capabilities?: string[];
|
|
116
|
-
}>;
|
|
117
|
-
edges: Array<unknown>;
|
|
118
|
+
const parameters = {
|
|
119
|
+
sourceText: 'The quick brown fox jumps over the lazy dog.',
|
|
120
|
+
targetLength: 'one-sentence',
|
|
121
|
+
tone: 'casual',
|
|
118
122
|
};
|
|
123
|
+
const res = await driver.post(EXPAND_PATH, { packName: SAMPLE_PACK, chainId: CHAIN_1_NODE, parameters });
|
|
124
|
+
expect(res.status).toBe(200);
|
|
125
|
+
const body = res.json as ExpandResponse;
|
|
119
126
|
|
|
120
127
|
expect(body.chainId).toBe(CHAIN_1_NODE);
|
|
121
128
|
expect(body.packName).toBe(SAMPLE_PACK);
|
|
122
|
-
expect(body.packVersion).toBe(
|
|
123
|
-
expect(body.nodes).toHaveLength(1);
|
|
124
|
-
expect(body.edges).toHaveLength(0);
|
|
129
|
+
expect(body.packVersion).toBe(PACK.version);
|
|
125
130
|
expect(typeof body.expansionId).toBe('string');
|
|
126
131
|
expect(body.expansionId.length).toBeGreaterThan(0);
|
|
127
132
|
|
|
133
|
+
// Derive the expected fragment from the reference library using the HOST's
|
|
134
|
+
// own expansionId — the host MUST reproduce the same algorithm output.
|
|
135
|
+
const expected = expandChain(chainById(CHAIN_1_NODE), {
|
|
136
|
+
expansionId: body.expansionId,
|
|
137
|
+
params: parameters,
|
|
138
|
+
isTypeIdResolvable: () => true,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(body.nodes).toHaveLength(expected.nodes.length);
|
|
142
|
+
expect(body.edges).toHaveLength(expected.edges.length);
|
|
143
|
+
|
|
128
144
|
const node = body.nodes[0]!;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
134
|
-
expect(node.typeId).toBe(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
expect(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
expect(node.capabilities).toEqual(['cacheable']);
|
|
145
|
+
const ref = expected.nodes[0]!;
|
|
146
|
+
expect(
|
|
147
|
+
node.id,
|
|
148
|
+
driver.describe('workflow-chain-packs.md §Expansion semantics', 'host rewrites the node id exactly as the reference library (chainId dots → underscores + expansionId prefix)'),
|
|
149
|
+
).toBe(ref.id);
|
|
150
|
+
expect(node.typeId).toBe(ref.typeId);
|
|
151
|
+
expect(
|
|
152
|
+
node.config?.systemPrompt,
|
|
153
|
+
driver.describe('workflow-chain-packs.md §Expansion semantics', 'host performs the same literal {{params.*}} substitution as the reference library'),
|
|
154
|
+
).toBe((ref.config as { systemPrompt?: string } | undefined)?.systemPrompt);
|
|
155
|
+
expect(
|
|
156
|
+
node.capabilities,
|
|
157
|
+
driver.describe('workflow-chain-packs.md §Capability propagation', 'chain capabilities propagate to the expanded node'),
|
|
158
|
+
).toEqual(ref.capabilities);
|
|
144
159
|
});
|
|
145
160
|
|
|
146
|
-
it('positive — 2-node chain
|
|
161
|
+
it('positive — 2-node chain matches the reference library (edge rewrite + capability propagation)', async () => {
|
|
147
162
|
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
148
163
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
chainId: CHAIN_2_NODE,
|
|
152
|
-
parameters: {
|
|
153
|
-
url: 'https://example.com/article',
|
|
154
|
-
targetLength: 'executive-summary',
|
|
155
|
-
},
|
|
156
|
-
});
|
|
164
|
+
const parameters = { url: 'https://example.com/article', targetLength: 'executive-summary' };
|
|
165
|
+
const res = await driver.post(EXPAND_PATH, { packName: SAMPLE_PACK, chainId: CHAIN_2_NODE, parameters });
|
|
157
166
|
expect(res.status).toBe(200);
|
|
167
|
+
const body = res.json as ExpandResponse;
|
|
158
168
|
|
|
159
|
-
const
|
|
160
|
-
expansionId:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
expect(body.
|
|
169
|
+
const expected = expandChain(chainById(CHAIN_2_NODE), {
|
|
170
|
+
expansionId: body.expansionId,
|
|
171
|
+
params: parameters,
|
|
172
|
+
isTypeIdResolvable: () => true,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(body.nodes).toHaveLength(expected.nodes.length); // 2
|
|
176
|
+
expect(body.edges).toHaveLength(expected.edges.length); // 1
|
|
166
177
|
|
|
167
|
-
// Both expanded nodes get the same prefix; the edge's `from`/`to`
|
|
168
|
-
// refer to fragment node ids and so get rewritten with the same
|
|
169
|
-
// prefix (port suffix preserved).
|
|
170
178
|
const edge = body.edges[0]!;
|
|
171
|
-
const
|
|
172
|
-
expect(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
const refEdge = expected.edges[0]!;
|
|
180
|
+
expect(
|
|
181
|
+
{ from: edge.from, to: edge.to },
|
|
182
|
+
driver.describe('workflow-chain-packs.md §Expansion semantics', 'host rewrites fragment-internal edge endpoints (port suffix preserved) exactly as the reference library'),
|
|
183
|
+
).toEqual({ from: refEdge.from, to: refEdge.to });
|
|
184
|
+
|
|
185
|
+
// side-effectful capability propagated to BOTH expanded nodes (per the ref).
|
|
186
|
+
const refCaps = expected.nodes.map((n) => n.capabilities);
|
|
187
|
+
expect(
|
|
188
|
+
body.nodes.map((n) => n.capabilities),
|
|
189
|
+
driver.describe('workflow-chain-packs.md §Capability propagation', 'chain capability propagates uniformly to every expanded node'),
|
|
190
|
+
).toEqual(refCaps);
|
|
179
191
|
});
|
|
180
192
|
|
|
181
193
|
it('negative — unknown pack returns 404 pack_not_found', async () => {
|
|
@@ -206,10 +218,7 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
|
|
|
206
218
|
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
207
219
|
|
|
208
220
|
// Missing chainId.
|
|
209
|
-
const res = await driver.post(EXPAND_PATH, {
|
|
210
|
-
packName: SAMPLE_PACK,
|
|
211
|
-
parameters: {},
|
|
212
|
-
});
|
|
221
|
+
const res = await driver.post(EXPAND_PATH, { packName: SAMPLE_PACK, parameters: {} });
|
|
213
222
|
expect(res.status).toBe(422);
|
|
214
223
|
expect((res.json as { error: string }).error).toBe('invalid_request');
|
|
215
224
|
});
|