@openwop/openwop-conformance 1.2.0 → 1.4.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 +156 -1
- package/README.md +3 -2
- package/api/asyncapi.yaml +8 -0
- package/api/openapi.yaml +371 -1
- package/api/redocly.yaml +15 -0
- package/coverage.md +26 -5
- 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-envelope-nl-to-format-engaged.json +41 -0
- package/fixtures/conformance-envelope-recovery-applied.json +39 -0
- package/fixtures/conformance-envelope-refusal.json +38 -0
- package/fixtures/conformance-envelope-retry-attempted.json +39 -0
- package/fixtures/conformance-envelope-retry-exhausted.json +38 -0
- package/fixtures/conformance-envelope-truncated.json +39 -0
- package/fixtures/conformance-envelope-truncation-cap-exhaustion.json +39 -0
- package/fixtures/conformance-model-capability-insufficient.json +25 -0
- package/fixtures/conformance-multi-agent-confidence-escalation.json +49 -0
- package/fixtures/conformance-multi-agent-handoff-child.json +27 -0
- package/fixtures/conformance-multi-agent-handoff.json +49 -0
- package/fixtures/conformance-prompt-all-four-kinds.json +39 -0
- package/fixtures/conformance-prompt-end-to-end.json +33 -0
- package/fixtures/conformance-subworkflow-input-mapping-no-default.json +33 -0
- package/fixtures/conformance-subworkflow-mid-run-mutation-child.json +31 -0
- package/fixtures/conformance-subworkflow-mid-run-mutation.json +33 -0
- package/fixtures/openwop-smoke-cost-emit.json +37 -0
- package/fixtures/prompt-templates/conformance-prompt-few-shot-2.json +14 -0
- package/fixtures/prompt-templates/conformance-prompt-few-shot.json +14 -0
- package/fixtures/prompt-templates/conformance-prompt-schema-hint.json +14 -0
- package/fixtures/prompt-templates/conformance-prompt-secret-redaction.json +23 -0
- package/fixtures/prompt-templates/conformance-prompt-trust-marker.json +23 -0
- package/fixtures/prompt-templates/conformance-prompt-writer-system.json +15 -0
- package/fixtures/prompt-templates/conformance-prompt-writer-user.json +15 -0
- package/fixtures.md +45 -0
- package/package.json +1 -1
- package/schemas/README.md +5 -0
- package/schemas/agent-manifest.schema.json +16 -0
- package/schemas/capabilities.schema.json +390 -0
- package/schemas/core-conformance-mock-agent-config.schema.json +5 -0
- package/schemas/envelopes/clarification.request.schema.json +9 -0
- package/schemas/envelopes/error.schema.json +4 -0
- package/schemas/envelopes/schema.request.schema.json +4 -0
- package/schemas/envelopes/schema.response.schema.json +1 -1
- package/schemas/node-pack-manifest.schema.json +28 -0
- package/schemas/orchestrator-decision.schema.json +12 -0
- package/schemas/prompt-kind.schema.json +8 -0
- package/schemas/prompt-pack-manifest.schema.json +80 -0
- package/schemas/prompt-ref.schema.json +40 -0
- package/schemas/prompt-template.schema.json +149 -0
- package/schemas/registry-version-manifest.schema.json +5 -0
- package/schemas/run-ancestry-response.schema.json +54 -0
- package/schemas/run-event-payloads.schema.json +513 -11
- package/schemas/run-event.schema.json +17 -1
- package/schemas/run-snapshot.schema.json +3 -2
- package/schemas/workflow-definition.schema.json +19 -1
- 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/llm-cache-key-recipe.ts +68 -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 +224 -15
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +257 -25
- package/src/scenarios/aiEnvelope.redaction.test.ts +210 -29
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +163 -24
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +262 -12
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +107 -16
- package/src/scenarios/blob-presign-expiry.test.ts +42 -9
- package/src/scenarios/blob-roundtrip.test.ts +0 -0
- package/src/scenarios/cache-ttl-expiry.test.ts +34 -8
- package/src/scenarios/cost-attribution.test.ts +124 -11
- package/src/scenarios/cross-engine-append-ordering.test.ts +99 -0
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +136 -0
- package/src/scenarios/cross-host-causation-shape.test.ts +117 -0
- package/src/scenarios/cross-host-traceparent-propagation.test.ts +60 -0
- 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/envelope-completion-distinguishes-truncation.test.ts +223 -0
- package/src/scenarios/envelope-nl-to-format-engaged.test.ts +152 -0
- package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +343 -0
- package/src/scenarios/envelope-reasoning-shape.test.ts +190 -0
- package/src/scenarios/envelope-recovery-applied.test.ts +229 -0
- package/src/scenarios/envelope-refusal-shape.test.ts +289 -0
- package/src/scenarios/envelope-retry-attempted.test.ts +258 -0
- package/src/scenarios/envelope-retry-exhausted.test.ts +168 -0
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +229 -0
- package/src/scenarios/envelope-truncated.test.ts +136 -0
- package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +144 -0
- package/src/scenarios/envelope-variant-discriminator-static.test.ts +152 -0
- package/src/scenarios/fixtures-gating.test.ts +139 -1
- package/src/scenarios/fixtures-valid.test.ts +123 -15
- package/src/scenarios/kv-ttl-expiry.test.ts +40 -9
- package/src/scenarios/model-capability-insufficient.test.ts +221 -0
- package/src/scenarios/model-capability-substituted.test.ts +203 -0
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +164 -0
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +167 -0
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +124 -0
- package/src/scenarios/multi-region-idempotency.test.ts +58 -0
- package/src/scenarios/node-module-required-capabilities-shape.test.ts +185 -0
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +19 -0
- package/src/scenarios/pack-registry-publish.test.ts +231 -51
- package/src/scenarios/prompt-all-four-kinds-events.test.ts +198 -0
- package/src/scenarios/prompt-composed-secret-redaction.test.ts +178 -0
- package/src/scenarios/prompt-composed-trust-marker.test.ts +165 -0
- package/src/scenarios/prompt-end-to-end-events.test.ts +202 -0
- package/src/scenarios/prompt-list-and-fetch.test.ts +207 -0
- package/src/scenarios/prompt-mutable-lifecycle.test.ts +216 -0
- package/src/scenarios/prompt-pack-install.test.ts +187 -0
- package/src/scenarios/prompt-render-deterministic.test.ts +240 -0
- package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +140 -0
- package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +172 -0
- package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +144 -0
- package/src/scenarios/prompt-template-shape.test.ts +359 -0
- package/src/scenarios/provider-usage.test.ts +185 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +64 -10
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +50 -10
- package/src/scenarios/replay-divergence-at-refusal.test.ts +134 -0
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +197 -0
- package/src/scenarios/replay-llm-cache-key.test.ts +127 -25
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +80 -0
- package/src/scenarios/sandbox-capability-gate-respected.test.ts +31 -0
- package/src/scenarios/sandbox-memory-cap.test.ts +61 -0
- package/src/scenarios/sandbox-no-cross-pack-mutation.test.ts +35 -0
- package/src/scenarios/sandbox-no-host-env-leak.test.ts +38 -0
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +91 -0
- package/src/scenarios/sandbox-no-host-process-escape.test.ts +30 -0
- package/src/scenarios/sandbox-no-network-escape.test.ts +49 -0
- package/src/scenarios/sandbox-timeout-cap.test.ts +61 -0
- package/src/scenarios/search-bm25-roundtrip.test.ts +54 -9
- package/src/scenarios/spec-corpus-validity.test.ts +34 -6
- package/src/scenarios/sql-transaction-atomicity.test.ts +37 -8
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +46 -9
- package/src/scenarios/subworkflow-input-mapping.test.ts +146 -10
- package/src/scenarios/table-cursor-pagination.test.ts +47 -9
- package/src/scenarios/table-schema-enforcement.test.ts +46 -9
- package/src/scenarios/vector-knn-roundtrip.test.ts +50 -10
- package/src/scenarios/workflow-chain-host-expansion.test.ts +202 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* vector-knn-roundtrip — RFC 0018 advertisement-shape verification + behavioral
|
|
2
|
+
* vector-knn-roundtrip — RFC 0018 advertisement-shape verification + behavioral roundtrip.
|
|
3
3
|
*
|
|
4
|
-
* Status: ACTIVE (advertisement-shape). RFC 0018 promoted to
|
|
5
|
-
* 2026-05-17. The matching `capabilities.vectorStore` block has
|
|
6
|
-
* `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
-
* shape against any host that boots the conformance suite, and
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape + behavioral). RFC 0018 promoted to
|
|
5
|
+
* `Active` 2026-05-17. The matching `capabilities.vectorStore` block has
|
|
6
|
+
* landed in `schemas/capabilities.schema.json`. This scenario asserts the
|
|
7
|
+
* advertisement shape against any host that boots the conformance suite, and
|
|
8
|
+
* exercises the behavioral surface through the `/v1/host/sample/test/surface`
|
|
9
|
+
* seam (soft-skip with HTTP 404 on hosts that don't expose it).
|
|
10
10
|
*
|
|
11
11
|
* Summary: upsert then query returns the same vectors in top-k order.
|
|
12
12
|
*
|
|
@@ -42,7 +42,47 @@ describe('vector-knn-roundtrip: advertisement shape (RFC 0018)', () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
46
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'vector', op, args });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('vector-knn-roundtrip: behavioral (RFC 0018 §A.vectorStore)', () => {
|
|
50
|
+
it('upsert 10 vectors → query with one of them returns it as the top match', async () => {
|
|
51
|
+
const probe = await call('query', { namespace: '__probe__', vector: [1, 0], topK: 1 });
|
|
52
|
+
if (probe.status === 404) return; // seam not exposed
|
|
53
|
+
const namespace = `knn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
54
|
+
const items = Array.from({ length: 10 }, (_, i) => ({
|
|
55
|
+
id: `vec-${i}`,
|
|
56
|
+
vector: [Math.cos((i * Math.PI) / 5), Math.sin((i * Math.PI) / 5)],
|
|
57
|
+
}));
|
|
58
|
+
const upsertRes = await call('upsert', { namespace, items });
|
|
59
|
+
expect(upsertRes.status).toBe(200);
|
|
60
|
+
|
|
61
|
+
const queryRes = await call('query', { namespace, vector: items[3]!.vector, topK: 1 });
|
|
62
|
+
expect(queryRes.status).toBe(200);
|
|
63
|
+
const body = queryRes.json as { matches?: Array<{ id?: string; score?: number }> };
|
|
64
|
+
expect(Array.isArray(body.matches), 'matches MUST be an array').toBe(true);
|
|
65
|
+
expect(body.matches!.length).toBeGreaterThan(0);
|
|
66
|
+
expect(
|
|
67
|
+
body.matches![0]!.id,
|
|
68
|
+
driver.describe('RFC 0018 §A.vectorStore', 'query with an indexed vector MUST return it as the top match'),
|
|
69
|
+
).toBe('vec-3');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('topK respects the configured limit', async () => {
|
|
73
|
+
const probe = await call('query', { namespace: '__probe__', vector: [1, 0], topK: 1 });
|
|
74
|
+
if (probe.status === 404) return;
|
|
75
|
+
const namespace = `topk-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
76
|
+
const items = Array.from({ length: 8 }, (_, i) => ({
|
|
77
|
+
id: `t-${i}`,
|
|
78
|
+
vector: [i / 10, 1 - i / 10],
|
|
79
|
+
}));
|
|
80
|
+
await call('upsert', { namespace, items });
|
|
81
|
+
const r3 = await call('query', { namespace, vector: [0.5, 0.5], topK: 3 });
|
|
82
|
+
const body = r3.json as { matches?: unknown[] };
|
|
83
|
+
expect(
|
|
84
|
+
Array.isArray(body.matches) && body.matches.length <= 3,
|
|
85
|
+
driver.describe('RFC 0018 §A.vectorStore', 'query MUST return at most topK matches'),
|
|
86
|
+
).toBe(true);
|
|
87
|
+
});
|
|
48
88
|
});
|
|
@@ -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
|
+
});
|