@openwop/openwop-conformance 1.1.0 → 1.2.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 +25 -0
- package/README.md +2 -2
- package/coverage.md +29 -17
- package/fixtures/conformance-agent-low-confidence.json +7 -4
- package/fixtures/conformance-agent-pack-handoff-schema-validation.json +30 -0
- package/fixtures/conformance-agent-reasoning.json +23 -4
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-a.json +27 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-b.json +25 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff.json +60 -0
- package/fixtures/conformance-dispatch-input-mapping-child.json +25 -0
- package/fixtures/conformance-dispatch-input-mapping.json +49 -0
- package/fixtures/conformance-dispatch-output-mapping-child.json +27 -0
- package/fixtures/conformance-dispatch-output-mapping.json +49 -0
- package/fixtures/conformance-subworkflow-input-mapping-child.json +27 -0
- package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
- package/fixtures.md +12 -2
- package/package.json +1 -1
- package/schemas/README.md +7 -0
- package/schemas/agent-ref.schema.json +1 -1
- package/schemas/ai-envelope.schema.json +106 -0
- package/schemas/capabilities.schema.json +300 -3
- package/schemas/core-conformance-mock-agent-config.schema.json +147 -0
- package/schemas/dispatch-config.schema.json +26 -0
- package/schemas/envelopes/clarification.request.schema.json +43 -0
- package/schemas/envelopes/error.schema.json +26 -0
- package/schemas/envelopes/schema.request.schema.json +22 -0
- package/schemas/envelopes/schema.response.schema.json +22 -0
- package/schemas/node-pack-manifest.schema.json +5 -0
- package/schemas/pack-lockfile.schema.json +16 -0
- package/schemas/run-event-payloads.schema.json +18 -2
- package/schemas/run-event.schema.json +2 -1
- package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
- package/src/lib/behavior-gate.ts +44 -5
- package/src/lib/env.ts +27 -0
- package/src/lib/webhook-receiver.ts +137 -0
- package/src/lib/workflow-chain-expansion.ts +213 -0
- package/src/scenarios/agentPackCatalog.test.ts +216 -0
- package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +146 -0
- package/src/scenarios/agentReasoningEvents.test.ts +58 -7
- package/src/scenarios/agents-run-tool-allowlist.test.ts +182 -0
- package/src/scenarios/ai-envelope-shape.test.ts +362 -0
- package/src/scenarios/aiEnvelope.capBreached.test.ts +173 -0
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +150 -0
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +69 -0
- package/src/scenarios/aiEnvelope.redaction.test.ts +73 -0
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +87 -0
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +143 -0
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +176 -0
- package/src/scenarios/append-ordering.test.ts +44 -0
- package/src/scenarios/artifact-auth.test.ts +58 -0
- package/src/scenarios/blob-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/blob-presign-expiry.test.ts +66 -0
- package/src/scenarios/blob-roundtrip.test.ts +48 -0
- package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
- package/src/scenarios/cache-ttl-expiry.test.ts +47 -0
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +98 -0
- package/src/scenarios/dispatch-input-mapping.test.ts +94 -0
- package/src/scenarios/dispatch-output-mapping.test.ts +65 -0
- package/src/scenarios/fs-path-traversal.test.ts +124 -0
- package/src/scenarios/idempotency-key-determinism.test.ts +230 -0
- package/src/scenarios/interrupt-token-matrix.test.ts +126 -0
- package/src/scenarios/kv-atomic-increment.test.ts +74 -0
- package/src/scenarios/kv-cas.test.ts +75 -0
- package/src/scenarios/kv-cross-tenant-isolation.test.ts +85 -0
- package/src/scenarios/kv-ttl-expiry.test.ts +47 -0
- package/src/scenarios/mcp-server-elicitation-bridge.test.ts +92 -0
- package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +80 -0
- package/src/scenarios/mcp-server-resource-roundtrip.test.ts +82 -0
- package/src/scenarios/mcp-server-sampling-bridge.test.ts +84 -0
- package/src/scenarios/mcp-server-tool-roundtrip.test.ts +107 -0
- package/src/scenarios/mcp-server-untrusted-args.test.ts +105 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +13 -6
- package/src/scenarios/memory-compaction-event-emitted.test.ts +121 -0
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +116 -0
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +127 -0
- package/src/scenarios/multi-region-idempotency.test.ts +39 -4
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +139 -0
- package/src/scenarios/pause-resume.test.ts +43 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +67 -0
- package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +48 -0
- package/src/scenarios/registry-public.test.ts +91 -0
- package/src/scenarios/search-bm25-roundtrip.test.ts +47 -0
- package/src/scenarios/spec-corpus-validity.test.ts +28 -7
- package/src/scenarios/sql-injection-rejection.test.ts +84 -0
- package/src/scenarios/sql-transaction-atomicity.test.ts +66 -0
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +66 -0
- package/src/scenarios/subworkflow-input-mapping.test.ts +100 -0
- package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
- package/src/scenarios/table-cursor-pagination.test.ts +47 -0
- package/src/scenarios/table-schema-enforcement.test.ts +47 -0
- package/src/scenarios/vector-knn-roundtrip.test.ts +48 -0
- package/src/scenarios/webhook-receiver-adversarial.test.ts +210 -0
- package/src/scenarios/workflow-chain-expansion.test.ts +366 -0
- package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +232 -0
- package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +138 -0
- package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +170 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow-chain pack signature verification — `workflow-chain-packs.md`
|
|
3
|
+
* §"Expansion semantics" step 2 + `node-packs.md` §Signing (reused
|
|
4
|
+
* verification flow, identical to node packs).
|
|
5
|
+
*
|
|
6
|
+
* Server-free scenario. Asserts that the Ed25519 signature verification
|
|
7
|
+
* recipe from `node-packs.md` §Signing — "`pack.json.sig` is an Ed25519
|
|
8
|
+
* signature over `pack.json` using the key at `keys/<key-id>.pem`" —
|
|
9
|
+
* works unchanged for workflow-chain pack manifests. The spec's design
|
|
10
|
+
* intent is that workflow-chain packs reuse 100% of the node-pack
|
|
11
|
+
* signing infrastructure (same algorithm, same byte recipe, same key
|
|
12
|
+
* format); this scenario proves the reuse is real, not just claimed.
|
|
13
|
+
*
|
|
14
|
+
* What this scenario covers:
|
|
15
|
+
* - A valid (manifest + signature) pair verifies cleanly.
|
|
16
|
+
* - A tampered manifest fails verification with the same shape of
|
|
17
|
+
* failure as a tampered node pack would produce.
|
|
18
|
+
* - A wrong-key signature fails verification.
|
|
19
|
+
*
|
|
20
|
+
* Why this matters: any host implementing chain expansion MUST verify
|
|
21
|
+
* the source pack's signature before resolving + expanding (step 2 of
|
|
22
|
+
* the expansion algorithm). If chain packs needed a different signing
|
|
23
|
+
* recipe than node packs, implementers would need two verification
|
|
24
|
+
* paths — that's a footgun. The spec explicitly designs them to share.
|
|
25
|
+
*
|
|
26
|
+
* @see spec/v1/workflow-chain-packs.md §"Expansion semantics" step 2
|
|
27
|
+
* @see spec/v1/node-packs.md §Signing
|
|
28
|
+
* @see RFCS/0013-workflow-chain-packs.md
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, it, expect } from 'vitest';
|
|
32
|
+
import { generateKeyPairSync, sign, verify } from 'node:crypto';
|
|
33
|
+
|
|
34
|
+
/** Canonical workflow-chain pack manifest used as the signing target.
|
|
35
|
+
* Mirrors the spec doc's Positive example, kept tight so the test
|
|
36
|
+
* focuses on the signing path, not the manifest shape. */
|
|
37
|
+
const MANIFEST = {
|
|
38
|
+
name: 'vendor.acme.editor-presets',
|
|
39
|
+
version: '1.0.0',
|
|
40
|
+
kind: 'workflow-chain',
|
|
41
|
+
description: 'Sample workflow-chain pack used by signature-verification conformance.',
|
|
42
|
+
engines: { openwop: '>=1.0.0 <2.0.0' },
|
|
43
|
+
chains: [
|
|
44
|
+
{
|
|
45
|
+
chainId: 'vendor.acme.generatePRD',
|
|
46
|
+
version: '1.0.0',
|
|
47
|
+
label: 'Generate PRD',
|
|
48
|
+
description: 'Single-node AI call.',
|
|
49
|
+
parameters: { type: 'object', properties: { productIdea: { type: 'string' } } },
|
|
50
|
+
dag: {
|
|
51
|
+
nodes: [{ id: 'prd-call', typeId: 'core.ai.callPrompt', config: {} }],
|
|
52
|
+
edges: [],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Canonical byte serialization of the manifest used for signing.
|
|
59
|
+
* Matches the recipe in node-packs.md §Signing: "Ed25519 signature
|
|
60
|
+
* over `pack.json`" — the on-disk JSON bytes ARE the signing payload.
|
|
61
|
+
* Production tooling would use the EXACT bytes from `pack.json` in the
|
|
62
|
+
* tarball (preserved whitespace, byte-for-byte) so the verification
|
|
63
|
+
* recipe is deterministic. */
|
|
64
|
+
function manifestBytes(): Buffer {
|
|
65
|
+
return Buffer.from(JSON.stringify(MANIFEST, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('category: workflow-chain pack signature — Ed25519 verification reuse from node-packs', () => {
|
|
69
|
+
it('valid manifest + valid signature MUST verify (positive path)', () => {
|
|
70
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
71
|
+
const manifest = manifestBytes();
|
|
72
|
+
// Per node-packs.md §Signing: Ed25519, no separate hash step (Ed25519
|
|
73
|
+
// signs the message directly). The `algorithm` arg to crypto.sign is
|
|
74
|
+
// `null` for Ed25519 — confirmed by Node's docs.
|
|
75
|
+
const signature = sign(null, manifest, privateKey);
|
|
76
|
+
const ok = verify(null, manifest, publicKey, signature);
|
|
77
|
+
expect(
|
|
78
|
+
ok,
|
|
79
|
+
'Per workflow-chain-packs.md §"Expansion semantics" step 2: the signature recipe is IDENTICAL to node-packs (Ed25519 over pack.json bytes). A valid pair MUST verify with the canonical recipe.',
|
|
80
|
+
).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('tampered manifest MUST fail verification (sha-level tamper detection)', () => {
|
|
84
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
85
|
+
const original = manifestBytes();
|
|
86
|
+
const signature = sign(null, original, privateKey);
|
|
87
|
+
// Tamper: change a single byte in the manifest after signing. This
|
|
88
|
+
// simulates a registry-side or man-in-the-middle modification — the
|
|
89
|
+
// signing recipe MUST detect ANY byte-level change.
|
|
90
|
+
const tampered = Buffer.from(original);
|
|
91
|
+
tampered[10] = tampered[10]! ^ 0x01;
|
|
92
|
+
const ok = verify(null, tampered, publicKey, signature);
|
|
93
|
+
expect(
|
|
94
|
+
ok,
|
|
95
|
+
'Per node-packs.md §Signing (reused unchanged for chain packs): Ed25519 verification MUST detect any byte-level tamper to the signed payload.',
|
|
96
|
+
).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('wrong-key signature MUST fail verification', () => {
|
|
100
|
+
const { privateKey: legitPrivKey } = generateKeyPairSync('ed25519');
|
|
101
|
+
const { publicKey: attackerPubKey } = generateKeyPairSync('ed25519');
|
|
102
|
+
const manifest = manifestBytes();
|
|
103
|
+
const signature = sign(null, manifest, legitPrivKey);
|
|
104
|
+
// Try to verify with a DIFFERENT public key than the one paired with
|
|
105
|
+
// the signing private key. This is the supply-chain attack the
|
|
106
|
+
// signing recipe defends against (attacker substitutes their own
|
|
107
|
+
// key in the manifest's `signing.publicKeyRef`).
|
|
108
|
+
const ok = verify(null, manifest, attackerPubKey, signature);
|
|
109
|
+
expect(
|
|
110
|
+
ok,
|
|
111
|
+
'Ed25519 verification MUST fail when the public key doesn\'t match the private key that produced the signature — the spec\'s trust model depends on this property.',
|
|
112
|
+
).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('chain pack manifests carry the SAME signing block shape as node packs', () => {
|
|
116
|
+
// node-packs.md §signing declares `pack.json` MAY carry a `signing`
|
|
117
|
+
// block with `publicKeyRef` / `signatureRef` / `method`. Chain packs
|
|
118
|
+
// reuse the same shape unchanged — workflow-chain-pack-manifest.
|
|
119
|
+
// schema.json's `$defs.Signing` is byte-identical to the node-pack
|
|
120
|
+
// schema's `$defs.Signing`. This assertion documents the spec
|
|
121
|
+
// design intent so future schema evolution doesn't silently
|
|
122
|
+
// diverge.
|
|
123
|
+
const signedManifest = {
|
|
124
|
+
...MANIFEST,
|
|
125
|
+
signing: {
|
|
126
|
+
publicKeyRef: 'keys/2026-05.pem',
|
|
127
|
+
signatureRef: 'pack.json.sig',
|
|
128
|
+
method: 'manual' as const,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
expect(
|
|
132
|
+
signedManifest.signing.method,
|
|
133
|
+
'workflow-chain packs MUST accept the same signing block keys as node packs (publicKeyRef + signatureRef + method) per spec design.',
|
|
134
|
+
).toBe('manual');
|
|
135
|
+
expect(signedManifest.signing.publicKeyRef).toBe('keys/2026-05.pem');
|
|
136
|
+
expect(signedManifest.signing.signatureRef).toBe('pack.json.sig');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow-chain expansion — unresolvable typeId rejection per
|
|
3
|
+
* `workflow-chain-packs.md` §"Expansion semantics (normative)" step 3
|
|
4
|
+
* + `workflow-chain-packs.md` §"Error codes" `chain_unresolvable_typeid`.
|
|
5
|
+
*
|
|
6
|
+
* Server-free scenario. Asserts that when a chain's `dag.nodes[].typeId`
|
|
7
|
+
* references an UNPUBLISHED typeId (one the destination host's pack
|
|
8
|
+
* registry can't resolve), expansion is rejected with
|
|
9
|
+
* `chain_unresolvable_typeid` BEFORE any node ids are rewritten or
|
|
10
|
+
* placeholders substituted.
|
|
11
|
+
*
|
|
12
|
+
* The "rejection at expansion time" path is load-bearing for the
|
|
13
|
+
* "additive — no new dispatch surface" invariant: if expansion produced
|
|
14
|
+
* a workflow referencing an unknown typeId, the dispatching engine
|
|
15
|
+
* would fail at run time with `unknown_typeid` and the user would see
|
|
16
|
+
* a confusing failure far from the chain-pack tile they dragged.
|
|
17
|
+
* Rejecting at edit time keeps the failure local to the author's
|
|
18
|
+
* action.
|
|
19
|
+
*
|
|
20
|
+
* Per the spec's "Negative: chain references unpublished typeId"
|
|
21
|
+
* example: the pack's manifest validation does NOT cross-check
|
|
22
|
+
* published typeId existence (cycle issues + registry-availability
|
|
23
|
+
* concerns); only the host-editor-time expansion step verifies — by
|
|
24
|
+
* which point the destination host's pack registry has authoritative
|
|
25
|
+
* knowledge of which typeIds it can resolve.
|
|
26
|
+
*
|
|
27
|
+
* @see spec/v1/workflow-chain-packs.md §"Expansion semantics" step 3
|
|
28
|
+
* @see spec/v1/workflow-chain-packs.md §"Error codes"
|
|
29
|
+
* @see conformance/src/lib/workflow-chain-expansion.ts
|
|
30
|
+
* @see RFCS/0013-workflow-chain-packs.md
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { describe, it, expect } from 'vitest';
|
|
34
|
+
import {
|
|
35
|
+
expandChain,
|
|
36
|
+
ChainUnresolvableTypeIdError,
|
|
37
|
+
type WorkflowChain,
|
|
38
|
+
} from '../lib/workflow-chain-expansion.js';
|
|
39
|
+
|
|
40
|
+
const CHAIN_WITH_UNKNOWN_TYPEID: WorkflowChain = {
|
|
41
|
+
chainId: 'vendor.acme.someChain',
|
|
42
|
+
version: '1.0.0',
|
|
43
|
+
label: 'Some Chain',
|
|
44
|
+
description: 'References an unpublished typeId — should fail expansion.',
|
|
45
|
+
parameters: {},
|
|
46
|
+
dag: {
|
|
47
|
+
nodes: [
|
|
48
|
+
{ id: 'n1', typeId: 'made.up.foo', config: {} },
|
|
49
|
+
],
|
|
50
|
+
edges: [],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Resolver that only accepts a hardcoded set of "known" typeIds — the
|
|
55
|
+
* set the destination host's pack registry can satisfy. Mimics the
|
|
56
|
+
* realistic host-editor-time check. */
|
|
57
|
+
const KNOWN_TYPEIDS = new Set(['core.identity', 'core.ai.callPrompt']);
|
|
58
|
+
const isKnown = (typeId: string): boolean => KNOWN_TYPEIDS.has(typeId);
|
|
59
|
+
|
|
60
|
+
describe('category: workflow-chain expansion — unresolvable typeId rejection', () => {
|
|
61
|
+
it('throws ChainUnresolvableTypeIdError when chain references an unknown typeId', () => {
|
|
62
|
+
expect(
|
|
63
|
+
() =>
|
|
64
|
+
expandChain(CHAIN_WITH_UNKNOWN_TYPEID, {
|
|
65
|
+
expansionId: 'x',
|
|
66
|
+
params: {},
|
|
67
|
+
isTypeIdResolvable: isKnown,
|
|
68
|
+
}),
|
|
69
|
+
'Per spec §"Expansion semantics" step 3: hosts MUST reject expansion with `chain_unresolvable_typeid` when any fragment node\'s typeId can\'t be resolved by the destination host.',
|
|
70
|
+
).toThrow(ChainUnresolvableTypeIdError);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('error carries the offending typeId AND the chainId in `details`', () => {
|
|
74
|
+
try {
|
|
75
|
+
expandChain(CHAIN_WITH_UNKNOWN_TYPEID, {
|
|
76
|
+
expansionId: 'x',
|
|
77
|
+
params: {},
|
|
78
|
+
isTypeIdResolvable: isKnown,
|
|
79
|
+
});
|
|
80
|
+
expect.fail('expandChain MUST have thrown ChainUnresolvableTypeIdError');
|
|
81
|
+
} catch (err) {
|
|
82
|
+
expect(err, 'expected ChainUnresolvableTypeIdError').toBeInstanceOf(
|
|
83
|
+
ChainUnresolvableTypeIdError,
|
|
84
|
+
);
|
|
85
|
+
const e = err as ChainUnresolvableTypeIdError;
|
|
86
|
+
expect(
|
|
87
|
+
e.code,
|
|
88
|
+
'Per spec §"Error codes": the wire-level error code MUST be `chain_unresolvable_typeid`.',
|
|
89
|
+
).toBe('chain_unresolvable_typeid');
|
|
90
|
+
expect(
|
|
91
|
+
e.typeId,
|
|
92
|
+
'The error MUST surface the offending typeId so the host editor can render an actionable diagnostic.',
|
|
93
|
+
).toBe('made.up.foo');
|
|
94
|
+
expect(
|
|
95
|
+
e.chainId,
|
|
96
|
+
'The error MUST surface the chainId so the host editor knows which chain-pack tile triggered the rejection.',
|
|
97
|
+
).toBe('vendor.acme.someChain');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('rejection happens BEFORE any id rewriting or placeholder substitution', () => {
|
|
102
|
+
// If the spec allowed expansion to proceed and produce a half-built
|
|
103
|
+
// fragment with an unknown typeId, downstream tooling would see a
|
|
104
|
+
// workflow with an undispatchable node. The contract is "reject
|
|
105
|
+
// cleanly at the resolution step, no partial output."
|
|
106
|
+
let threw = false;
|
|
107
|
+
try {
|
|
108
|
+
expandChain(CHAIN_WITH_UNKNOWN_TYPEID, {
|
|
109
|
+
expansionId: 'x',
|
|
110
|
+
params: { foo: 'bar' },
|
|
111
|
+
isTypeIdResolvable: isKnown,
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
threw = true;
|
|
115
|
+
}
|
|
116
|
+
expect(
|
|
117
|
+
threw,
|
|
118
|
+
'Expansion MUST throw before producing any output — host editors rely on the "no partial expansion" guarantee to keep parent workflows clean on rejection.',
|
|
119
|
+
).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('accepts the chain when every typeId IS resolvable', () => {
|
|
123
|
+
const resolvable: WorkflowChain = {
|
|
124
|
+
...CHAIN_WITH_UNKNOWN_TYPEID,
|
|
125
|
+
dag: {
|
|
126
|
+
nodes: [{ id: 'n1', typeId: 'core.identity', config: {} }],
|
|
127
|
+
edges: [],
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
expect(
|
|
131
|
+
() =>
|
|
132
|
+
expandChain(resolvable, {
|
|
133
|
+
expansionId: 'x',
|
|
134
|
+
params: {},
|
|
135
|
+
isTypeIdResolvable: isKnown,
|
|
136
|
+
}),
|
|
137
|
+
'Sanity: when every typeId resolves, expansion MUST proceed without throwing.',
|
|
138
|
+
).not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('throws on the FIRST unknown typeId encountered (fail-fast)', () => {
|
|
142
|
+
// Chain with two nodes — one resolvable, one not. The unresolvable one
|
|
143
|
+
// is second. The throw MUST identify the second one (the actual
|
|
144
|
+
// offender), not silently skip it.
|
|
145
|
+
const mixed: WorkflowChain = {
|
|
146
|
+
...CHAIN_WITH_UNKNOWN_TYPEID,
|
|
147
|
+
dag: {
|
|
148
|
+
nodes: [
|
|
149
|
+
{ id: 'n1', typeId: 'core.identity', config: {} },
|
|
150
|
+
{ id: 'n2', typeId: 'made.up.foo', config: {} },
|
|
151
|
+
],
|
|
152
|
+
edges: [],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
expandChain(mixed, {
|
|
157
|
+
expansionId: 'x',
|
|
158
|
+
params: {},
|
|
159
|
+
isTypeIdResolvable: isKnown,
|
|
160
|
+
});
|
|
161
|
+
expect.fail('MUST have thrown on n2\'s unknown typeId');
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const e = err as ChainUnresolvableTypeIdError;
|
|
164
|
+
expect(
|
|
165
|
+
e.typeId,
|
|
166
|
+
'When multiple nodes have unknown typeIds, the throw MUST identify the first unknown one encountered in declaration order.',
|
|
167
|
+
).toBe('made.up.foo');
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|