@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.
@@ -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 (never bundled
10
- * into `@openwop/openwop-conformance`, so no host has been handed it — the
11
- * scenario soft-skips everywhere until a serving host advertises the seam).
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 spec-authoritative
15
- * `expandChain()` reference library.
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 the capability (precondition for the rest).
28
- * 2. Positive — 1-node chain expands; substituted config + rewritten
29
- * id + propagated capabilities match the pure-library output for
30
- * the same input.
31
- * 3. Positive 2-node chain with edges expands; edge endpoints
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 capabilities.md §workflowChainPacks
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 SAMPLE_PACK = 'vendor.openwop.workflow-chain-sample';
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
- const EXPAND_PATH = '/v1/host/sample/workflow-chain:expand';
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 via the host returns substituted config + rewritten id', async () => {
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 res = await driver.post(EXPAND_PATH, {
96
- packName: SAMPLE_PACK,
97
- chainId: CHAIN_1_NODE,
98
- parameters: {
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('1.0.0');
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
- // Step 6: id rewriting — chainId's dots become underscores +
130
- // expansionId suffix + original fragment id.
131
- expect(node.id).toMatch(
132
- /^vendor_openwop_workflow-chain-sample_summarize-text_[a-f0-9]+_summarize-call$/,
133
- );
134
- expect(node.typeId).toBe('core.ai.callPrompt');
135
-
136
- // Step 5: literal substitution.
137
- const sysPrompt = node.config?.systemPrompt ?? '';
138
- expect(sysPrompt).toContain('a one-sentence summary');
139
- expect(sysPrompt).toContain('a casual tone');
140
- expect(sysPrompt).toContain('The quick brown fox jumps over the lazy dog.');
141
-
142
- // Step 8: capability propagation.
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 with edges expands with rewritten edge endpoints', async () => {
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 res = await driver.post(EXPAND_PATH, {
150
- packName: SAMPLE_PACK,
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 body = res.json as {
160
- expansionId: string;
161
- nodes: Array<{ id: string; typeId: string; capabilities?: string[] }>;
162
- edges: Array<{ from: string; to: string }>;
163
- };
164
- expect(body.nodes).toHaveLength(2);
165
- expect(body.edges).toHaveLength(1);
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 prefix = `vendor_openwop_workflow-chain-sample_fetch-and-summarize_${body.expansionId}_`;
172
- expect(edge.from).toBe(`${prefix}fetch.body`);
173
- expect(edge.to).toBe(`${prefix}summarize.sourceText`);
174
-
175
- // side-effectful capability propagated to BOTH expanded nodes.
176
- for (const node of body.nodes) {
177
- expect(node.capabilities, `node ${node.id} inherits chain capability`).toEqual(['side-effectful']);
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
  });