capdag 1.226.626 → 1.240.679
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/cap-fab-renderer.js +126 -61
- package/capdag.js +209 -80
- package/capdag.test.js +622 -103
- package/package.json +1 -1
package/capdag.test.js
CHANGED
|
@@ -89,6 +89,19 @@ function assertThrowsMediaUrn(fn, expectedErrorCode, message) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function assertThrowsMessage(fn, expectedMessage, message) {
|
|
93
|
+
try {
|
|
94
|
+
fn();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
assert(
|
|
97
|
+
typeof error.message === 'string' && error.message.includes(expectedMessage),
|
|
98
|
+
`${message}: expected error containing ${JSON.stringify(expectedMessage)}, got ${JSON.stringify(error.message)}`
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`Assertion failed: ${message}: expected function to throw`);
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
function runTest(name, fn) {
|
|
93
106
|
try {
|
|
94
107
|
const result = fn();
|
|
@@ -126,14 +139,14 @@ function test6204_Urn(tags) {
|
|
|
126
139
|
// Helper to create a Cap for testing
|
|
127
140
|
function makeCap(urnString, title) {
|
|
128
141
|
const capUrn = CapUrn.fromString(urnString);
|
|
129
|
-
return new Cap(capUrn, title, 'test', title);
|
|
142
|
+
return new Cap(capUrn, title, ['test'], title);
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
// Helper to create caps with specific in/out media URNs for graph testing
|
|
133
146
|
function makeGraphCap(inUrn, outUrn, title) {
|
|
134
147
|
const urnString = `cap:in="${inUrn}";convert;out="${outUrn}"`;
|
|
135
148
|
const capUrn = CapUrn.fromString(urnString);
|
|
136
|
-
return new Cap(capUrn, title, 'convert', title);
|
|
149
|
+
return new Cap(capUrn, title, ['convert'], title);
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
// ============================================================================
|
|
@@ -715,7 +728,7 @@ function test050_matchingSemanticsDirectionMismatch() {
|
|
|
715
728
|
assert(!cap.accepts(request), 'Incompatible direction types should not match');
|
|
716
729
|
}
|
|
717
730
|
|
|
718
|
-
// TEST890: Semantic direction matching - generic
|
|
731
|
+
// TEST890: Semantic direction matching - generic candidate matches specific request
|
|
719
732
|
function test890_directionSemanticMatching() {
|
|
720
733
|
// Generic wildcard cap accepts specific pdf request
|
|
721
734
|
const genericCap = CapUrn.fromString(
|
|
@@ -1271,7 +1284,7 @@ function test116_capArgConstructors() {
|
|
|
1271
1284
|
// TEST150: JSON roundtrip
|
|
1272
1285
|
function test150_capManifestJsonSerialization() {
|
|
1273
1286
|
const capUrn = CapUrn.fromString(test6204_Urn('extract;target=metadata'));
|
|
1274
|
-
const cap = new Cap(capUrn, 'Extract Metadata', 'extract-metadata');
|
|
1287
|
+
const cap = new Cap(capUrn, 'Extract Metadata', ['extract-metadata']);
|
|
1275
1288
|
cap.addArg(new CapArg('media:ext=pdf', true, [new ArgSource({ stdin: 'media:ext=pdf' })]));
|
|
1276
1289
|
cap.addArg(new CapArg(
|
|
1277
1290
|
'media:chunk-size;enc=utf-8;numeric',
|
|
@@ -1409,14 +1422,14 @@ function test6208_CapFabGetOutgoingConformsToMatching() {
|
|
|
1409
1422
|
// the renderer colours/groups edges by provenance in browse mode.
|
|
1410
1423
|
function test6224_CapFabDistinctRegistryNames() {
|
|
1411
1424
|
const graph = new CapFab();
|
|
1412
|
-
graph.addCap(makeGraphCap('media:ext=pdf', 'media:enc=utf-8', 'PDF to Text'), '
|
|
1425
|
+
graph.addCap(makeGraphCap('media:ext=pdf', 'media:enc=utf-8', 'PDF to Text'), 'bundled-cartridges');
|
|
1413
1426
|
graph.addCap(makeGraphCap('media:enc=utf-8', 'media:embedding-vector', 'Embed'), 'cartridges');
|
|
1414
1427
|
|
|
1415
1428
|
const edges = graph.getEdges();
|
|
1416
1429
|
assertEqual(edges.length, 2, 'Two caps must produce two edges');
|
|
1417
1430
|
|
|
1418
1431
|
const names = new Set(edges.map(e => e.registryName));
|
|
1419
|
-
assert(names.has('
|
|
1432
|
+
assert(names.has('bundled-cartridges'), 'bundled-cartridges registry name must be preserved');
|
|
1420
1433
|
assert(names.has('cartridges'), 'cartridges registry name must be preserved');
|
|
1421
1434
|
}
|
|
1422
1435
|
|
|
@@ -1719,7 +1732,7 @@ function test6242_JS_resolveMediaUrnFromSpecs() {
|
|
|
1719
1732
|
// TEST6246: J s cap j s o n serialization
|
|
1720
1733
|
function test6246_JS_capJSONSerialization() {
|
|
1721
1734
|
const urn = CapUrn.fromString(test6204_Urn('test'));
|
|
1722
|
-
const cap = new Cap(urn, 'Test Cap', 'test_command');
|
|
1735
|
+
const cap = new Cap(urn, 'Test Cap', ['test_command']);
|
|
1723
1736
|
cap.arguments = {
|
|
1724
1737
|
required: [{ name: 'input', media_urn: MEDIA_STRING }],
|
|
1725
1738
|
optional: []
|
|
@@ -1742,7 +1755,7 @@ function test6246_JS_capJSONSerialization() {
|
|
|
1742
1755
|
// surface as failures here.
|
|
1743
1756
|
function test6249_JS_capDocumentationRoundTrip() {
|
|
1744
1757
|
const urn = CapUrn.fromString(test6204_Urn('documented'));
|
|
1745
|
-
const cap = new Cap(urn, 'Documented Cap', 'documented');
|
|
1758
|
+
const cap = new Cap(urn, 'Documented Cap', ['documented']);
|
|
1746
1759
|
const body = '# Documented Cap\r\n\nDoes the thing.\n\n```bash\necho "hi"\n```\n\nSee also: \u2605\n';
|
|
1747
1760
|
cap.setDocumentation(body);
|
|
1748
1761
|
assertEqual(cap.getDocumentation(), body, 'Setter must store the body verbatim');
|
|
@@ -1764,7 +1777,7 @@ function test6249_JS_capDocumentationRoundTrip() {
|
|
|
1764
1777
|
// (which has no null sentinel) and pollute generated JSON.
|
|
1765
1778
|
function test6253_JS_capDocumentationOmittedWhenNull() {
|
|
1766
1779
|
const urn = CapUrn.fromString(test6204_Urn('undocumented'));
|
|
1767
|
-
const cap = new Cap(urn, 'Undocumented Cap', 'undocumented');
|
|
1780
|
+
const cap = new Cap(urn, 'Undocumented Cap', ['undocumented']);
|
|
1768
1781
|
assertEqual(cap.getDocumentation(), null, 'Default documentation must be null');
|
|
1769
1782
|
|
|
1770
1783
|
const json = cap.toJSON();
|
|
@@ -1854,8 +1867,10 @@ function test6269_JS_mediaDefConstruction() {
|
|
|
1854
1867
|
// to assert on. Tests that need an empty channel use literals inline.
|
|
1855
1868
|
const sampleRegistry = {
|
|
1856
1869
|
schemaVersion: '5.0',
|
|
1870
|
+
registryVersion: 1,
|
|
1857
1871
|
lastUpdated: '2026-02-07T16:48:28Z',
|
|
1858
1872
|
registryUrl: 'https://test.example/manifest',
|
|
1873
|
+
fabricRegistryUrl: 'https://fabric.test',
|
|
1859
1874
|
channels: {
|
|
1860
1875
|
release: { cartridges: {
|
|
1861
1876
|
pdfcartridge: {
|
|
@@ -2089,7 +2104,7 @@ function test323_cartridgeRepoServerValidateRegistry() {
|
|
|
2089
2104
|
// Missing channels object
|
|
2090
2105
|
threw = false;
|
|
2091
2106
|
try {
|
|
2092
|
-
new CartridgeRepoServer({schemaVersion: '5.0', registryUrl: 'https://test.example/manifest'});
|
|
2107
|
+
new CartridgeRepoServer({schemaVersion: '5.0', registryVersion: 1, fabricRegistryUrl: 'https://fabric.test', registryUrl: 'https://test.example/manifest'});
|
|
2093
2108
|
} catch (e) {
|
|
2094
2109
|
threw = true;
|
|
2095
2110
|
assert(e.message.includes('channels'), 'Should reject missing channels');
|
|
@@ -2099,7 +2114,7 @@ function test323_cartridgeRepoServerValidateRegistry() {
|
|
|
2099
2114
|
// Missing one of the two required channels
|
|
2100
2115
|
threw = false;
|
|
2101
2116
|
try {
|
|
2102
|
-
new CartridgeRepoServer({schemaVersion: '5.0', registryUrl: 'https://test.example/manifest', channels: {release: {cartridges: {}}}});
|
|
2117
|
+
new CartridgeRepoServer({schemaVersion: '5.0', registryVersion: 1, fabricRegistryUrl: 'https://fabric.test', registryUrl: 'https://test.example/manifest', channels: {release: {cartridges: {}}}});
|
|
2103
2118
|
} catch (e) {
|
|
2104
2119
|
threw = true;
|
|
2105
2120
|
assert(e.message.includes('nightly'), 'Should require nightly channel');
|
|
@@ -2581,7 +2596,9 @@ function test1874_registryUrlFromBuildEnvRejectsEmptyString() {
|
|
|
2581
2596
|
function discoveryInstallFixture(root, slug, channelFolder, name, version, cartridgeJson, entry) {
|
|
2582
2597
|
const fs = require('fs');
|
|
2583
2598
|
const path = require('path');
|
|
2584
|
-
|
|
2599
|
+
// {slug}/v1/{channel}/{name}/{version} — the version level pins to the host
|
|
2600
|
+
// registry version (the tests construct identities at cartridgeRegistryVersion 1).
|
|
2601
|
+
const dir = path.join(root, slug, 'v1', channelFolder, name, version);
|
|
2585
2602
|
fs.mkdirSync(dir, { recursive: true });
|
|
2586
2603
|
if (cartridgeJson !== null && cartridgeJson !== undefined) {
|
|
2587
2604
|
fs.writeFileSync(path.join(dir, 'cartridge.json'), cartridgeJson);
|
|
@@ -2634,7 +2651,7 @@ async function test1875_scanAllReachesBothDevAndRegistrySlugs() {
|
|
|
2634
2651
|
const url = 'https://cartridges.example.com/manifest';
|
|
2635
2652
|
const rslug = slugForSync(url);
|
|
2636
2653
|
// Host baked for a DIFFERENT registry than the on-disk registry cartridge.
|
|
2637
|
-
const host = new DiscoveryIdentity({ channel: 'nightly', registryUrl: 'https://other.example.com/manifest', fabricManifestVersion: 1 });
|
|
2654
|
+
const host = new DiscoveryIdentity({ channel: 'nightly', registryUrl: 'https://other.example.com/manifest', fabricManifestVersion: 1, cartridgeRegistryVersion: 1 });
|
|
2638
2655
|
discoveryInstallFixture(root, 'dev', 'nightly', 'devcart', '1.0.0', discoveryDevCartridgeJson('nightly', 1), 'cart');
|
|
2639
2656
|
discoveryInstallFixture(root, rslug, 'nightly', 'regcart', '1.0.0', discoveryRegistryCartridgeJson(url, 'nightly', 1), 'cart');
|
|
2640
2657
|
const out = await discoverCartridges(root, host);
|
|
@@ -2658,7 +2675,7 @@ async function test1876_otherChannelSubtreeIsSkipped() {
|
|
|
2658
2675
|
const url = 'https://cartridges.example.com/manifest';
|
|
2659
2676
|
const rslug = slugForSync(url);
|
|
2660
2677
|
discoveryInstallFixture(root, rslug, 'release', 'regcart', '1.0.0', discoveryRegistryCartridgeJson(url, 'release', 1), 'cart');
|
|
2661
|
-
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1 }));
|
|
2678
|
+
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
|
|
2662
2679
|
assertEqual(out.length, 0, 'a release-only slug must be invisible to a nightly host');
|
|
2663
2680
|
} finally {
|
|
2664
2681
|
discoveryRemoveRoot(root);
|
|
@@ -2675,7 +2692,7 @@ async function test1877_registryCartridgeUnderWrongSlugIsBadInstall() {
|
|
|
2675
2692
|
const wrongSlug = slugForSync('https://somewhere-else.example.com/manifest');
|
|
2676
2693
|
const json = discoveryRegistryCartridgeJson(url, 'nightly', 1);
|
|
2677
2694
|
discoveryInstallFixture(root, wrongSlug, 'nightly', 'cart', '1.0.0', json, 'cart');
|
|
2678
|
-
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1 }));
|
|
2695
|
+
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
|
|
2679
2696
|
discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.BAD_INSTALLATION);
|
|
2680
2697
|
} finally {
|
|
2681
2698
|
discoveryRemoveRoot(root);
|
|
@@ -2683,11 +2700,11 @@ async function test1877_registryCartridgeUnderWrongSlugIsBadInstall() {
|
|
|
2683
2700
|
}
|
|
2684
2701
|
|
|
2685
2702
|
// TEST1878: a cartridge marked `installed_from: bundle` with no baked hash in
|
|
2686
|
-
//
|
|
2703
|
+
// BUNDLED_CARTRIDGE_HASHES (empty in this mirror) is rejected as BadInstallation
|
|
2687
2704
|
// — the bundled-integrity gate fires before the probe. Non-macOS only: on macOS
|
|
2688
2705
|
// the baked-hash path is intentionally absent (OS code-signature is the guard),
|
|
2689
|
-
// so a bundled
|
|
2690
|
-
async function
|
|
2706
|
+
// so a bundled cartridge is accepted there and would instead end at the probe.
|
|
2707
|
+
async function test1878_bundledCartridgeWithoutBakedHashIsRejected() {
|
|
2691
2708
|
if (process.platform === 'darwin') {
|
|
2692
2709
|
// Mirrors the Rust #[cfg(not(target_os = "macos"))] gate: on macOS the
|
|
2693
2710
|
// baked-hash path does not exist, so this scenario is not exercised.
|
|
@@ -2703,9 +2720,9 @@ async function test1878_bundledProviderWithoutBakedHashIsRejected() {
|
|
|
2703
2720
|
installed_at: '2024-01-01T00:00:00Z', installed_from: 'bundle', fabric_manifest_version: 1,
|
|
2704
2721
|
});
|
|
2705
2722
|
discoveryInstallFixture(root, 'dev', 'nightly', 'cart', '1.0.0', json, 'cart');
|
|
2706
|
-
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1 }));
|
|
2723
|
+
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
|
|
2707
2724
|
discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.BAD_INSTALLATION);
|
|
2708
|
-
assert(out[0].error.message.includes('bundled
|
|
2725
|
+
assert(out[0].error.message.includes('bundled cartridge integrity'),
|
|
2709
2726
|
`message should name the bundled-integrity failure: ${out[0].error.message}`);
|
|
2710
2727
|
} finally {
|
|
2711
2728
|
discoveryRemoveRoot(root);
|
|
@@ -2954,7 +2971,7 @@ function test6544_builderRejectsStructuralKeys() {
|
|
|
2954
2971
|
// TEST1294: RULE11 - void-input cap with stdin source rejected
|
|
2955
2972
|
function test1294_rule11VoidInputWithStdinRejected() {
|
|
2956
2973
|
const urn = CapUrn.fromString('cap:in="media:void";test;out="media:string"');
|
|
2957
|
-
const cap = new Cap(urn, 'Test', 'test-cmd');
|
|
2974
|
+
const cap = new Cap(urn, 'Test', ['test-cmd']);
|
|
2958
2975
|
const stdinSource = ArgSource.fromJSON({ stdin: 'media:string' });
|
|
2959
2976
|
cap.args = [new CapArg('media:string', true, [stdinSource])];
|
|
2960
2977
|
try {
|
|
@@ -2969,7 +2986,7 @@ function test1294_rule11VoidInputWithStdinRejected() {
|
|
|
2969
2986
|
// TEST1295: RULE11 - non-void-input cap without stdin source rejected
|
|
2970
2987
|
function test1295_rule11NonVoidInputWithoutStdinRejected() {
|
|
2971
2988
|
const urn = CapUrn.fromString('cap:in="media:string";test;out="media:string"');
|
|
2972
|
-
const cap = new Cap(urn, 'Test', 'test-cmd');
|
|
2989
|
+
const cap = new Cap(urn, 'Test', ['test-cmd']);
|
|
2973
2990
|
const posSource = ArgSource.fromJSON({ cli_flag: '--name' });
|
|
2974
2991
|
cap.args = [new CapArg('media:string', true, [posSource])];
|
|
2975
2992
|
try {
|
|
@@ -2984,7 +3001,7 @@ function test1295_rule11NonVoidInputWithoutStdinRejected() {
|
|
|
2984
3001
|
// TEST1296: RULE11 - void-input cap with only cli_flag sources passes
|
|
2985
3002
|
function test1296_rule11VoidInputCliFlagOnly() {
|
|
2986
3003
|
const urn = CapUrn.fromString('cap:in="media:void";test;out="media:string"');
|
|
2987
|
-
const cap = new Cap(urn, 'Test', 'test-cmd');
|
|
3004
|
+
const cap = new Cap(urn, 'Test', ['test-cmd']);
|
|
2988
3005
|
const flagSource = ArgSource.fromJSON({ cli_flag: '--name' });
|
|
2989
3006
|
cap.args = [new CapArg('media:string', true, [flagSource])];
|
|
2990
3007
|
// Should not throw
|
|
@@ -2994,7 +3011,7 @@ function test1296_rule11VoidInputCliFlagOnly() {
|
|
|
2994
3011
|
// TEST1297: RULE11 - non-void-input cap with stdin source passes
|
|
2995
3012
|
function test1297_rule11NonVoidInputWithStdin() {
|
|
2996
3013
|
const urn = CapUrn.fromString('cap:in="media:string";test;out="media:string"');
|
|
2997
|
-
const cap = new Cap(urn, 'Test', 'test-cmd');
|
|
3014
|
+
const cap = new Cap(urn, 'Test', ['test-cmd']);
|
|
2998
3015
|
const stdinSource = ArgSource.fromJSON({ stdin: 'media:string' });
|
|
2999
3016
|
cap.args = [new CapArg('media:string', true, [stdinSource])];
|
|
3000
3017
|
// Should not throw
|
|
@@ -3191,11 +3208,11 @@ function test127_invalidEffectNoneFailsHard() {
|
|
|
3191
3208
|
|
|
3192
3209
|
// TEST128: omitted effect means declared; unconstrained effect must be explicit
|
|
3193
3210
|
function test128_effectDispatchRequiresExplicitWildcard() {
|
|
3194
|
-
const
|
|
3211
|
+
const noneCandidate = CapUrn.fromString('cap:effect=none');
|
|
3195
3212
|
const declaredRequest = CapUrn.fromString('cap:raw');
|
|
3196
3213
|
const anyRequest = CapUrn.fromString('cap:?effect');
|
|
3197
|
-
assert(!
|
|
3198
|
-
assert(
|
|
3214
|
+
assert(!noneCandidate.isDispatchable(declaredRequest), 'effect=none should not silently satisfy declared request');
|
|
3215
|
+
assert(noneCandidate.isDispatchable(anyRequest), 'explicit ?effect should accept any candidate effect');
|
|
3199
3216
|
}
|
|
3200
3217
|
|
|
3201
3218
|
// ============================================================================
|
|
@@ -4292,7 +4309,7 @@ function test6467_Machine_capRegistryEntry_construction() {
|
|
|
4292
4309
|
command: 'extract',
|
|
4293
4310
|
cap_description: 'Extracts text from PDF',
|
|
4294
4311
|
args: [{ media_urn: 'media:ext=pdf', required: true }],
|
|
4295
|
-
output: { media_urn: 'media:enc=utf-8;ext=txt', output_description: '
|
|
4312
|
+
output: { media_urn: 'media:enc=utf-8;ext=txt', output_description: 'Digitized text' },
|
|
4296
4313
|
media_defs: [],
|
|
4297
4314
|
urn_tags: { op: 'extract' },
|
|
4298
4315
|
in_spec: 'media:ext=pdf',
|
|
@@ -4337,7 +4354,7 @@ function test6470_Machine_capRegistryEntry_defaults() {
|
|
|
4337
4354
|
assertEqual(entry.urn, 'cap:in=media:;test;out=media:', 'URN should match');
|
|
4338
4355
|
assertEqual(entry.title, '', 'Title should default to empty');
|
|
4339
4356
|
assertEqual(entry.description, '', 'Description should default to empty');
|
|
4340
|
-
|
|
4357
|
+
assert(Array.isArray(entry.aliases) && entry.aliases.length === 0, 'Aliases should default to an empty array');
|
|
4341
4358
|
assert(Array.isArray(entry.args), 'Args should default to array');
|
|
4342
4359
|
assertEqual(entry.args.length, 0, 'Args should be empty');
|
|
4343
4360
|
}
|
|
@@ -4563,8 +4580,16 @@ function test6483_Renderer_buildBrowseGraphData_rejectsMissingMediaTitles() {
|
|
|
4563
4580
|
|
|
4564
4581
|
// ---------------- strand builder ----------------
|
|
4565
4582
|
|
|
4583
|
+
let fixtureStepToken = 0;
|
|
4584
|
+
|
|
4585
|
+
function nextFixtureStepToken() {
|
|
4586
|
+
fixtureStepToken += 1;
|
|
4587
|
+
return `fixture-step-${fixtureStepToken}`;
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4566
4590
|
function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
|
|
4567
4591
|
return {
|
|
4592
|
+
token_id: nextFixtureStepToken(),
|
|
4568
4593
|
step_type: {
|
|
4569
4594
|
Cap: {
|
|
4570
4595
|
cap_urn: capUrn,
|
|
@@ -4572,6 +4597,7 @@ function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
|
|
|
4572
4597
|
specificity: 0,
|
|
4573
4598
|
input_is_sequence: inSeq,
|
|
4574
4599
|
output_is_sequence: outSeq,
|
|
4600
|
+
inputs: [{ arg_urn: fromSpec, source: 'StrandInput' }],
|
|
4575
4601
|
},
|
|
4576
4602
|
},
|
|
4577
4603
|
from_spec: fromSpec,
|
|
@@ -4581,6 +4607,7 @@ function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
|
|
|
4581
4607
|
|
|
4582
4608
|
function makeForEachStep(mediaDef) {
|
|
4583
4609
|
return {
|
|
4610
|
+
token_id: nextFixtureStepToken(),
|
|
4584
4611
|
step_type: { ForEach: { media_def: mediaDef } },
|
|
4585
4612
|
from_spec: mediaDef,
|
|
4586
4613
|
to_spec: mediaDef,
|
|
@@ -4589,12 +4616,34 @@ function makeForEachStep(mediaDef) {
|
|
|
4589
4616
|
|
|
4590
4617
|
function makeCollectStep(mediaDef) {
|
|
4591
4618
|
return {
|
|
4619
|
+
token_id: nextFixtureStepToken(),
|
|
4592
4620
|
step_type: { Collect: { media_def: mediaDef } },
|
|
4593
4621
|
from_spec: mediaDef,
|
|
4594
4622
|
to_spec: mediaDef,
|
|
4595
4623
|
};
|
|
4596
4624
|
}
|
|
4597
4625
|
|
|
4626
|
+
// Build an intentionally linear fixture while still spelling the canonical
|
|
4627
|
+
// stable-token producer graph that production payloads carry. ForEach changes
|
|
4628
|
+
// cardinality but does not replace the current value producer; Collect does.
|
|
4629
|
+
function wireLinearStepInputs(steps) {
|
|
4630
|
+
let producerToken = null;
|
|
4631
|
+
for (const step of steps) {
|
|
4632
|
+
if (step.step_type.Cap) {
|
|
4633
|
+
step.step_type.Cap.inputs = [{
|
|
4634
|
+
arg_urn: step.from_spec,
|
|
4635
|
+
source: producerToken === null
|
|
4636
|
+
? 'StrandInput'
|
|
4637
|
+
: { Step: { token_id: producerToken } },
|
|
4638
|
+
}];
|
|
4639
|
+
producerToken = step.token_id;
|
|
4640
|
+
} else if (step.step_type.Collect) {
|
|
4641
|
+
producerToken = step.token_id;
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
return steps;
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4598
4647
|
// TEST6484: Renderer validate strand step rejects unknown variant
|
|
4599
4648
|
function test6484_Renderer_validateStrandStep_rejectsUnknownVariant() {
|
|
4600
4649
|
// A step with an unknown variant must fail hard at validation; no
|
|
@@ -4602,6 +4651,7 @@ function test6484_Renderer_validateStrandStep_rejectsUnknownVariant() {
|
|
|
4602
4651
|
let threw = false;
|
|
4603
4652
|
try {
|
|
4604
4653
|
rendererValidateStrandStep({
|
|
4654
|
+
token_id: nextFixtureStepToken(),
|
|
4605
4655
|
step_type: { WrongVariant: {} },
|
|
4606
4656
|
from_spec: 'media:a',
|
|
4607
4657
|
to_spec: 'media:a',
|
|
@@ -4620,6 +4670,7 @@ function test6486_Renderer_validateStrandStep_requiresBooleanIsSequence() {
|
|
|
4620
4670
|
let threw = false;
|
|
4621
4671
|
try {
|
|
4622
4672
|
rendererValidateStrandStep({
|
|
4673
|
+
token_id: nextFixtureStepToken(),
|
|
4623
4674
|
step_type: { Cap: {
|
|
4624
4675
|
cap_urn: 'cap:in="media:a";x;out="media:b"',
|
|
4625
4676
|
title: 't',
|
|
@@ -4641,13 +4692,13 @@ function test6487_Renderer_classifyStrandCapSteps_capFlags() {
|
|
|
4641
4692
|
// Strand: ForEach → cap1 → cap2 → cap3 → Collect. cap1 should have
|
|
4642
4693
|
// prevForEach=true; cap3 should have nextCollect=true; cap2 should
|
|
4643
4694
|
// have neither.
|
|
4644
|
-
const steps = [
|
|
4695
|
+
const steps = wireLinearStepInputs([
|
|
4645
4696
|
makeForEachStep('media:ext=pdf;list'),
|
|
4646
4697
|
makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=png;image"', 'a', 'media:ext=pdf', 'media:ext=png;image', false, false),
|
|
4647
4698
|
makeCapStep('cap:in="media:ext=png;image";b;out="media:ext=jpg"', 'b', 'media:ext=png;image', 'media:ext=jpg', false, false),
|
|
4648
4699
|
makeCapStep('cap:in="media:ext=jpg";c;out="media:ext=txt"', 'c', 'media:ext=jpg', 'media:ext=txt', false, false),
|
|
4649
4700
|
makeCollectStep('media:ext=txt'),
|
|
4650
|
-
];
|
|
4701
|
+
]);
|
|
4651
4702
|
const { capStepIndices, capFlags } = rendererClassifyStrandCapSteps(steps);
|
|
4652
4703
|
assertEqual(capStepIndices.length, 3, 'three cap steps');
|
|
4653
4704
|
assert(capFlags.get(1).prevForEach, 'cap1 has prevForEach');
|
|
@@ -4663,7 +4714,7 @@ function test6488_Renderer_classifyStrandCapSteps_nestedForks() {
|
|
|
4663
4714
|
// Nested strand: ForEach → cap1 → ForEach → cap2 → Collect → cap3 → Collect.
|
|
4664
4715
|
// cap1 has prevForEach (outer), cap2 has prevForEach (inner) and
|
|
4665
4716
|
// nextCollect (inner), cap3 has nextCollect (outer).
|
|
4666
|
-
const steps = [
|
|
4717
|
+
const steps = wireLinearStepInputs([
|
|
4667
4718
|
makeForEachStep('media:a;list'),
|
|
4668
4719
|
makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4669
4720
|
makeForEachStep('media:b;list'),
|
|
@@ -4671,7 +4722,7 @@ function test6488_Renderer_classifyStrandCapSteps_nestedForks() {
|
|
|
4671
4722
|
makeCollectStep('media:c'),
|
|
4672
4723
|
makeCapStep('cap:in="media:c";c;out="media:d"', 'c', 'media:c', 'media:d', false, false),
|
|
4673
4724
|
makeCollectStep('media:d'),
|
|
4674
|
-
];
|
|
4725
|
+
]);
|
|
4675
4726
|
const { capFlags } = rendererClassifyStrandCapSteps(steps);
|
|
4676
4727
|
assert(capFlags.get(1).prevForEach && !capFlags.get(1).nextCollect, 'cap1 outer entry');
|
|
4677
4728
|
assert(capFlags.get(3).prevForEach && capFlags.get(3).nextCollect, 'cap2 inner both');
|
|
@@ -4698,9 +4749,9 @@ function test6489_Renderer_buildStrandGraphData_singleCapPlain() {
|
|
|
4698
4749
|
const payload = withMediaDisplayNames({
|
|
4699
4750
|
source_media_urn: 'media:a',
|
|
4700
4751
|
target_media_urn: 'media:b',
|
|
4701
|
-
steps: [
|
|
4752
|
+
steps: wireLinearStepInputs([
|
|
4702
4753
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4703
|
-
],
|
|
4754
|
+
]),
|
|
4704
4755
|
}, {
|
|
4705
4756
|
'media:a': 'Source A',
|
|
4706
4757
|
'media:b': 'Target B',
|
|
@@ -4724,9 +4775,9 @@ function test6491_Renderer_buildStrandGraphData_sequenceShowsCardinality() {
|
|
|
4724
4775
|
const payload = withMediaDisplayNames({
|
|
4725
4776
|
source_media_urn: 'media:a;list',
|
|
4726
4777
|
target_media_urn: 'media:b',
|
|
4727
|
-
steps: [
|
|
4778
|
+
steps: wireLinearStepInputs([
|
|
4728
4779
|
makeCapStep('cap:in="media:a;list";x;out="media:b"', 'x', 'media:a;list', 'media:b', true, false),
|
|
4729
|
-
],
|
|
4780
|
+
]),
|
|
4730
4781
|
}, {
|
|
4731
4782
|
'media:a;list': 'Source A List',
|
|
4732
4783
|
'media:b': 'Target B',
|
|
@@ -4755,11 +4806,11 @@ function test6492_Renderer_buildStrandGraphData_foreachCollectSpan() {
|
|
|
4755
4806
|
const payload = withMediaDisplayNames({
|
|
4756
4807
|
source_media_urn: 'media:ext=pdf;list',
|
|
4757
4808
|
target_media_urn: 'media:txt;list',
|
|
4758
|
-
steps: [
|
|
4809
|
+
steps: wireLinearStepInputs([
|
|
4759
4810
|
makeForEachStep('media:ext=pdf;list'),
|
|
4760
4811
|
makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
|
|
4761
4812
|
makeCollectStep('media:ext=txt'),
|
|
4762
|
-
],
|
|
4813
|
+
]),
|
|
4763
4814
|
}, {
|
|
4764
4815
|
'media:ext=pdf;list': 'PDF List',
|
|
4765
4816
|
'media:ext=txt': 'Plain Text',
|
|
@@ -4798,10 +4849,10 @@ function test6493_Renderer_buildStrandGraphData_standaloneCollect() {
|
|
|
4798
4849
|
const payload = withMediaDisplayNames({
|
|
4799
4850
|
source_media_urn: 'media:a',
|
|
4800
4851
|
target_media_urn: 'media:b;list',
|
|
4801
|
-
steps: [
|
|
4852
|
+
steps: wireLinearStepInputs([
|
|
4802
4853
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4803
4854
|
makeCollectStep('media:b'),
|
|
4804
|
-
],
|
|
4855
|
+
]),
|
|
4805
4856
|
}, {
|
|
4806
4857
|
'media:a': 'Source A',
|
|
4807
4858
|
'media:b': 'Target B',
|
|
@@ -4827,11 +4878,11 @@ function test6494_Renderer_buildStrandGraphData_unclosedForEachBody() {
|
|
|
4827
4878
|
const payload = withMediaDisplayNames({
|
|
4828
4879
|
source_media_urn: 'media:a',
|
|
4829
4880
|
target_media_urn: 'media:c',
|
|
4830
|
-
steps: [
|
|
4881
|
+
steps: wireLinearStepInputs([
|
|
4831
4882
|
makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4832
4883
|
makeForEachStep('media:b'),
|
|
4833
4884
|
makeCapStep('cap:in="media:b";b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
|
|
4834
|
-
],
|
|
4885
|
+
]),
|
|
4835
4886
|
}, {
|
|
4836
4887
|
'media:a': 'Source A',
|
|
4837
4888
|
'media:b': 'Intermediate B',
|
|
@@ -4865,11 +4916,11 @@ function test6495_Renderer_buildStrandGraphData_nestedForEachThrows() {
|
|
|
4865
4916
|
const payload = withMediaDisplayNames({
|
|
4866
4917
|
source_media_urn: 'media:a;list',
|
|
4867
4918
|
target_media_urn: 'media:a',
|
|
4868
|
-
steps: [
|
|
4919
|
+
steps: wireLinearStepInputs([
|
|
4869
4920
|
makeForEachStep('media:a;list'),
|
|
4870
4921
|
makeForEachStep('media:a'),
|
|
4871
4922
|
makeCapStep('cap:in="media:a";x;out="media:a"', 'x', 'media:a', 'media:a', false, false),
|
|
4872
|
-
],
|
|
4923
|
+
]),
|
|
4873
4924
|
}, {
|
|
4874
4925
|
'media:a;list': 'Source A List',
|
|
4875
4926
|
'media:a': 'Source A',
|
|
@@ -4903,11 +4954,11 @@ function test6496_Renderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
|
|
|
4903
4954
|
const payload = withMediaDisplayNames({
|
|
4904
4955
|
source_media_urn: 'media:ext=pdf;list',
|
|
4905
4956
|
target_media_urn: 'media:txt;list',
|
|
4906
|
-
steps: [
|
|
4957
|
+
steps: wireLinearStepInputs([
|
|
4907
4958
|
makeForEachStep('media:ext=pdf;list'),
|
|
4908
4959
|
makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
|
|
4909
4960
|
makeCollectStep('media:ext=txt'),
|
|
4910
|
-
],
|
|
4961
|
+
]),
|
|
4911
4962
|
}, {
|
|
4912
4963
|
'media:ext=pdf;list': 'PDF List',
|
|
4913
4964
|
'media:ext=txt': 'Plain Text',
|
|
@@ -4953,11 +5004,11 @@ function test6497_Renderer_collapseStrand_unclosedForEachBodyCollapses() {
|
|
|
4953
5004
|
const payload = withMediaDisplayNames({
|
|
4954
5005
|
source_media_urn: 'media:a',
|
|
4955
5006
|
target_media_urn: 'media:c',
|
|
4956
|
-
steps: [
|
|
5007
|
+
steps: wireLinearStepInputs([
|
|
4957
5008
|
makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4958
5009
|
makeForEachStep('media:b'),
|
|
4959
5010
|
makeCapStep('cap:in="media:b";b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
|
|
4960
|
-
],
|
|
5011
|
+
]),
|
|
4961
5012
|
}, {
|
|
4962
5013
|
'media:a': 'Source A',
|
|
4963
5014
|
'media:b': 'Intermediate B',
|
|
@@ -5011,10 +5062,10 @@ function test6498_Renderer_collapseStrand_standaloneCollectCollapses() {
|
|
|
5011
5062
|
const payload = withMediaDisplayNames({
|
|
5012
5063
|
source_media_urn: 'media:a',
|
|
5013
5064
|
target_media_urn: 'media:b;list',
|
|
5014
|
-
steps: [
|
|
5065
|
+
steps: wireLinearStepInputs([
|
|
5015
5066
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
5016
5067
|
makeCollectStep('media:b'),
|
|
5017
|
-
],
|
|
5068
|
+
]),
|
|
5018
5069
|
}, {
|
|
5019
5070
|
'media:a': 'Source A',
|
|
5020
5071
|
'media:b': 'Target B',
|
|
@@ -5060,11 +5111,11 @@ function test6499_Renderer_collapseStrand_sequenceProducingCapBeforeForeach() {
|
|
|
5060
5111
|
const payload = withMediaDisplayNames({
|
|
5061
5112
|
source_media_urn: 'media:ext=pdf',
|
|
5062
5113
|
target_media_urn: 'media:decision',
|
|
5063
|
-
steps: [
|
|
5114
|
+
steps: wireLinearStepInputs([
|
|
5064
5115
|
makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
|
|
5065
5116
|
makeForEachStep('media:page'),
|
|
5066
5117
|
makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
5067
|
-
],
|
|
5118
|
+
]),
|
|
5068
5119
|
}, {
|
|
5069
5120
|
'media:ext=pdf': 'PDF',
|
|
5070
5121
|
'media:page': 'Page',
|
|
@@ -5116,9 +5167,9 @@ function test6500_Renderer_collapseStrand_plainCapMergesTrailingOutput() {
|
|
|
5116
5167
|
const payload = withMediaDisplayNames({
|
|
5117
5168
|
source_media_urn: 'media:a',
|
|
5118
5169
|
target_media_urn: 'media:b',
|
|
5119
|
-
steps: [
|
|
5170
|
+
steps: wireLinearStepInputs([
|
|
5120
5171
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
5121
|
-
],
|
|
5172
|
+
]),
|
|
5122
5173
|
}, {
|
|
5123
5174
|
'media:a': 'Source A',
|
|
5124
5175
|
'media:b': 'Target B',
|
|
@@ -5149,9 +5200,9 @@ function test6501_Renderer_collapseStrand_plainCapDistinctTargetNoMerge() {
|
|
|
5149
5200
|
const payload = withMediaDisplayNames({
|
|
5150
5201
|
source_media_urn: 'media:a',
|
|
5151
5202
|
target_media_urn: 'media:b;list',
|
|
5152
|
-
steps: [
|
|
5203
|
+
steps: wireLinearStepInputs([
|
|
5153
5204
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
5154
|
-
],
|
|
5205
|
+
]),
|
|
5155
5206
|
}, {
|
|
5156
5207
|
'media:a': 'Source A',
|
|
5157
5208
|
'media:b': 'Target B',
|
|
@@ -5180,24 +5231,175 @@ function test6502_Renderer_validateStrandPayload_missingSourceMediaUrn() {
|
|
|
5180
5231
|
assert(threw, 'missing source_media_urn must throw');
|
|
5181
5232
|
}
|
|
5182
5233
|
|
|
5234
|
+
// TEST7120: Renderer requires the authoritative cap input graph
|
|
5235
|
+
function test7120_Renderer_validateStrandPayload_requiresAuthoritativeInputs() {
|
|
5236
|
+
const step = makeCapStep(
|
|
5237
|
+
'cap:in="media:a";x;out="media:b"',
|
|
5238
|
+
'x', 'media:a', 'media:b', false, false
|
|
5239
|
+
);
|
|
5240
|
+
const payload = { source_media_urn: 'media:a', target_media_urn: 'media:b', steps: [step] };
|
|
5241
|
+
|
|
5242
|
+
const missing = JSON.parse(JSON.stringify(payload));
|
|
5243
|
+
delete missing.steps[0].step_type.Cap.inputs;
|
|
5244
|
+
assertThrowsMessage(
|
|
5245
|
+
() => rendererValidateStrandPayload(missing),
|
|
5246
|
+
'inputs must be an array',
|
|
5247
|
+
'a cap cannot omit its data-flow graph'
|
|
5248
|
+
);
|
|
5249
|
+
|
|
5250
|
+
const empty = JSON.parse(JSON.stringify(payload));
|
|
5251
|
+
empty.steps[0].step_type.Cap.inputs = [];
|
|
5252
|
+
assertThrowsMessage(
|
|
5253
|
+
() => rendererValidateStrandPayload(empty),
|
|
5254
|
+
'must include the primary input',
|
|
5255
|
+
'a cap cannot omit its primary input'
|
|
5256
|
+
);
|
|
5257
|
+
|
|
5258
|
+
const duplicate = JSON.parse(JSON.stringify(payload));
|
|
5259
|
+
duplicate.steps[0].step_type.Cap.inputs.push(
|
|
5260
|
+
{ arg_urn: 'media:a', source: 'StrandInput' }
|
|
5261
|
+
);
|
|
5262
|
+
assertThrowsMessage(
|
|
5263
|
+
() => rendererValidateStrandPayload(duplicate),
|
|
5264
|
+
"duplicates 'media:a'",
|
|
5265
|
+
'one cap cannot bind the same argument slot twice'
|
|
5266
|
+
);
|
|
5267
|
+
|
|
5268
|
+
const unresolved = JSON.parse(JSON.stringify(payload));
|
|
5269
|
+
unresolved.steps[0].step_type.Cap.inputs[0].source = { Step: { token_id: 'missing-step' } };
|
|
5270
|
+
assertThrowsMessage(
|
|
5271
|
+
() => rendererValidateStrandPayload(unresolved),
|
|
5272
|
+
'does not resolve to a strand step',
|
|
5273
|
+
'a cap input cannot name an unknown producer'
|
|
5274
|
+
);
|
|
5275
|
+
}
|
|
5276
|
+
|
|
5183
5277
|
// ---------------- run builder ----------------
|
|
5184
5278
|
|
|
5185
5279
|
function test6503_Renderer_validateBodyOutcome_rejectsNegativeIndex() {
|
|
5186
5280
|
let threw = false;
|
|
5187
5281
|
try {
|
|
5188
|
-
rendererValidateBodyOutcome({ body_index: -1, success: true, cap_urns: [] }, 'test');
|
|
5282
|
+
rendererValidateBodyOutcome({ body_index: -1, foreach_token_id: null, success: true, cap_urns: [], failed_token_id: null, failed_arg_urn: null }, 'test');
|
|
5189
5283
|
} catch (e) {
|
|
5190
5284
|
threw = true;
|
|
5191
5285
|
}
|
|
5192
5286
|
assert(threw, 'negative body_index must throw');
|
|
5193
5287
|
}
|
|
5194
5288
|
|
|
5289
|
+
// TEST7121: Renderer body outcomes require an explicit stable failure coordinate
|
|
5290
|
+
function test7121_Renderer_validateBodyOutcome_requiresExplicitFailureCoordinate() {
|
|
5291
|
+
rendererValidateBodyOutcome(
|
|
5292
|
+
{ body_index: 0, foreach_token_id: null, success: false, cap_urns: [], failed_token_id: null, failed_arg_urn: null },
|
|
5293
|
+
'unattributed'
|
|
5294
|
+
);
|
|
5295
|
+
let threw = false;
|
|
5296
|
+
try {
|
|
5297
|
+
rendererValidateBodyOutcome({ body_index: 0, success: false, cap_urns: [] }, 'missing');
|
|
5298
|
+
} catch (error) {
|
|
5299
|
+
threw = true;
|
|
5300
|
+
assert(error.message.includes('foreach_token_id must be present as null or a stable ForEach step token'),
|
|
5301
|
+
'missing execution-region coordinate error must state the required wire shape');
|
|
5302
|
+
}
|
|
5303
|
+
assert(threw, 'omitting foreach_token_id must fail hard');
|
|
5304
|
+
|
|
5305
|
+
threw = false;
|
|
5306
|
+
try {
|
|
5307
|
+
rendererValidateBodyOutcome(
|
|
5308
|
+
{ body_index: 0, foreach_token_id: null, success: false, cap_urns: [], failed_arg_urn: null },
|
|
5309
|
+
'missing-failure-coordinate'
|
|
5310
|
+
);
|
|
5311
|
+
} catch (error) {
|
|
5312
|
+
threw = true;
|
|
5313
|
+
assert(error.message.includes('must be present as null or a stable step token'),
|
|
5314
|
+
'missing failure coordinate error must state the required wire shape');
|
|
5315
|
+
}
|
|
5316
|
+
assert(threw, 'omitting failed_token_id must fail hard');
|
|
5317
|
+
|
|
5318
|
+
threw = false;
|
|
5319
|
+
try {
|
|
5320
|
+
rendererValidateBodyOutcome(
|
|
5321
|
+
{ body_index: 0, foreach_token_id: null, success: false, cap_urns: [], failed_token_id: null },
|
|
5322
|
+
'missing-argument-coordinate'
|
|
5323
|
+
);
|
|
5324
|
+
} catch (error) {
|
|
5325
|
+
threw = true;
|
|
5326
|
+
assert(error.message.includes('must be present as null or an attributed argument URN'),
|
|
5327
|
+
'missing argument coordinate error must state the required wire shape');
|
|
5328
|
+
}
|
|
5329
|
+
assert(threw, 'omitting failed_arg_urn must fail hard');
|
|
5330
|
+
|
|
5331
|
+
assertThrowsMessage(
|
|
5332
|
+
() => rendererValidateBodyOutcome(
|
|
5333
|
+
{ body_index: 1, foreach_token_id: null, success: true, cap_urns: [], failed_token_id: null, failed_arg_urn: null },
|
|
5334
|
+
'nonzero-linear'
|
|
5335
|
+
),
|
|
5336
|
+
'linear and must use body_index 0',
|
|
5337
|
+
'a bare ordinal cannot address a second linear body'
|
|
5338
|
+
);
|
|
5339
|
+
|
|
5340
|
+
assertThrowsMessage(
|
|
5341
|
+
() => rendererValidateBodyOutcome(
|
|
5342
|
+
{ body_index: Number.MAX_SAFE_INTEGER + 1, foreach_token_id: 'tok-foreach', success: true, cap_urns: [], failed_token_id: null, failed_arg_urn: null },
|
|
5343
|
+
'unsafe-coordinate'
|
|
5344
|
+
),
|
|
5345
|
+
'non-negative safe integer',
|
|
5346
|
+
'JSON transport must not round a body coordinate'
|
|
5347
|
+
);
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5350
|
+
// TEST7122: Renderer body outcomes reject execution-region tokens for non-ForEach steps
|
|
5351
|
+
function test7122_Renderer_validateRunPayload_requiresForEachRegionCoordinate() {
|
|
5352
|
+
const strand = {
|
|
5353
|
+
source_media_urn: 'media:a',
|
|
5354
|
+
target_media_urn: 'media:b',
|
|
5355
|
+
steps: wireLinearStepInputs([
|
|
5356
|
+
makeCapStep('cap:in="media:a";convert;out="media:b"', 'convert', 'media:a', 'media:b', false, false),
|
|
5357
|
+
]),
|
|
5358
|
+
};
|
|
5359
|
+
const payload = {
|
|
5360
|
+
resolved_strand: strand,
|
|
5361
|
+
body_outcomes: [{
|
|
5362
|
+
body_index: 0,
|
|
5363
|
+
foreach_token_id: strand.steps[0].token_id,
|
|
5364
|
+
success: true,
|
|
5365
|
+
cap_urns: [],
|
|
5366
|
+
failed_token_id: null,
|
|
5367
|
+
failed_arg_urn: null,
|
|
5368
|
+
}],
|
|
5369
|
+
visible_success_count: 1,
|
|
5370
|
+
visible_failure_count: 0,
|
|
5371
|
+
total_body_count: 1,
|
|
5372
|
+
};
|
|
5373
|
+
assertThrowsMessage(
|
|
5374
|
+
() => rendererValidateRunPayload(payload),
|
|
5375
|
+
'does not resolve to a ForEach strand step',
|
|
5376
|
+
'a body outcome cannot use a cap token as its execution-region coordinate'
|
|
5377
|
+
);
|
|
5378
|
+
|
|
5379
|
+
const duplicateCoordinate = {
|
|
5380
|
+
...payload,
|
|
5381
|
+
body_outcomes: [0, 1].map(() => ({
|
|
5382
|
+
body_index: 0,
|
|
5383
|
+
foreach_token_id: null,
|
|
5384
|
+
success: true,
|
|
5385
|
+
cap_urns: [],
|
|
5386
|
+
failed_token_id: null,
|
|
5387
|
+
failed_arg_urn: null,
|
|
5388
|
+
})),
|
|
5389
|
+
};
|
|
5390
|
+
assertThrowsMessage(
|
|
5391
|
+
() => rendererValidateRunPayload(duplicateCoordinate),
|
|
5392
|
+
"duplicate body coordinate 'linear:0'",
|
|
5393
|
+
'one execution coordinate cannot acquire two outcomes'
|
|
5394
|
+
);
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5195
5397
|
// TEST6504: Renderer build run graph data pages successes and failures
|
|
5196
5398
|
function test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
|
|
5197
5399
|
// 6 successes, 4 failures. visible=3+2, total=10. Body has 2
|
|
5198
5400
|
// caps (a, b). Each body replica is a chain of:
|
|
5199
5401
|
// entry node + body_step_0 (cap a) + body_step_1 (cap b)
|
|
5200
|
-
// = 3 nodes per body. Failed bodies truncate at
|
|
5402
|
+
// = 3 nodes per body. Failed bodies truncate at failed_token_id
|
|
5201
5403
|
// (cap b, idx=1) so `traceEnd=2` — same 3-node chain.
|
|
5202
5404
|
//
|
|
5203
5405
|
// Total replica nodes: (3 success × 3) + (2 failure × 3) = 15.
|
|
@@ -5207,26 +5409,28 @@ function test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
|
|
|
5207
5409
|
const strand = {
|
|
5208
5410
|
source_media_urn: 'media:ext=pdf;list',
|
|
5209
5411
|
target_media_urn: 'media:ext=txt',
|
|
5210
|
-
steps: [
|
|
5412
|
+
steps: wireLinearStepInputs([
|
|
5211
5413
|
makeForEachStep('media:ext=pdf;list'),
|
|
5212
5414
|
makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=png;image"', 'a', 'media:ext=pdf', 'media:ext=png;image', false, false),
|
|
5213
5415
|
makeCapStep('cap:in="media:ext=png;image";b;out="media:ext=txt"', 'b', 'media:ext=png;image', 'media:ext=txt', false, false),
|
|
5214
5416
|
makeCollectStep('media:ext=txt'),
|
|
5215
|
-
],
|
|
5417
|
+
]),
|
|
5216
5418
|
};
|
|
5217
5419
|
const outcomes = [];
|
|
5218
5420
|
for (let i = 0; i < 6; i++) {
|
|
5219
|
-
outcomes.push({ body_index: i, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0 });
|
|
5421
|
+
outcomes.push({ body_index: i, foreach_token_id: strand.steps[0].token_id, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: null, failed_arg_urn: null });
|
|
5220
5422
|
}
|
|
5221
5423
|
for (let i = 6; i < 10; i++) {
|
|
5222
5424
|
outcomes.push({
|
|
5223
5425
|
body_index: i,
|
|
5426
|
+
foreach_token_id: strand.steps[0].token_id,
|
|
5224
5427
|
success: false,
|
|
5225
5428
|
cap_urns: [],
|
|
5226
5429
|
saved_paths: [],
|
|
5227
5430
|
total_bytes: 0,
|
|
5228
5431
|
duration_ms: 0,
|
|
5229
|
-
|
|
5432
|
+
failed_token_id: strand.steps[2].token_id,
|
|
5433
|
+
failed_arg_urn: null,
|
|
5230
5434
|
error: 'oom',
|
|
5231
5435
|
});
|
|
5232
5436
|
}
|
|
@@ -5263,22 +5467,21 @@ function test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
|
|
|
5263
5467
|
assertEqual(failureShowMore.data.hiddenCount, 2, 'failure hidden count = 2');
|
|
5264
5468
|
}
|
|
5265
5469
|
|
|
5266
|
-
// TEST6505: Renderer
|
|
5267
|
-
function
|
|
5268
|
-
// A failure without
|
|
5269
|
-
//
|
|
5270
|
-
// crash or produce zero replicas.
|
|
5470
|
+
// TEST6505: Renderer run graph keeps an unattributed body failure at its entry
|
|
5471
|
+
function test6505_Renderer_buildRunGraphData_unattributedFailureStopsAtEntry() {
|
|
5472
|
+
// A failure without failed_token_id is machine-scoped. The body remains
|
|
5473
|
+
// visible, but no cap is falsely presented as the failure location.
|
|
5271
5474
|
//
|
|
5272
5475
|
// Strand [ForEach, Cap, Collect] → body has 1 cap. Each body
|
|
5273
5476
|
// replica emits 1 entry node + 1 body cap node = 2 nodes.
|
|
5274
5477
|
const strand = {
|
|
5275
5478
|
source_media_urn: 'media:ext=pdf;list',
|
|
5276
5479
|
target_media_urn: 'media:ext=txt',
|
|
5277
|
-
steps: [
|
|
5480
|
+
steps: wireLinearStepInputs([
|
|
5278
5481
|
makeForEachStep('media:ext=pdf;list'),
|
|
5279
5482
|
makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=txt"', 'a', 'media:ext=pdf', 'media:ext=txt', false, false),
|
|
5280
5483
|
makeCollectStep('media:ext=txt'),
|
|
5281
|
-
],
|
|
5484
|
+
]),
|
|
5282
5485
|
};
|
|
5283
5486
|
const payload = {
|
|
5284
5487
|
resolved_strand: strand,
|
|
@@ -5288,7 +5491,7 @@ function test6505_Renderer_buildRunGraphData_failureWithoutFailedCapRendersFullT
|
|
|
5288
5491
|
'media:ext=txt': 'Text',
|
|
5289
5492
|
},
|
|
5290
5493
|
body_outcomes: [
|
|
5291
|
-
{ body_index: 0, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, error: 'unknown' },
|
|
5494
|
+
{ body_index: 0, foreach_token_id: strand.steps[0].token_id, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: null, failed_arg_urn: null, error: 'unknown' },
|
|
5292
5495
|
],
|
|
5293
5496
|
visible_success_count: 0,
|
|
5294
5497
|
visible_failure_count: 5,
|
|
@@ -5299,19 +5502,15 @@ function test6505_Renderer_buildRunGraphData_failureWithoutFailedCapRendersFullT
|
|
|
5299
5502
|
for (const n of built.replicaNodes) {
|
|
5300
5503
|
if (n.classes === 'body-failure') failureNodes++;
|
|
5301
5504
|
}
|
|
5302
|
-
assertEqual(failureNodes,
|
|
5505
|
+
assertEqual(failureNodes, 1, 'only the body entry is shown for an unattributed failure');
|
|
5303
5506
|
}
|
|
5304
5507
|
|
|
5305
|
-
// TEST6506: Renderer
|
|
5306
|
-
function
|
|
5307
|
-
// The renderer matches failed_cap against step cap URNs via
|
|
5308
|
-
// CapUrn.isEquivalent, NOT string equality. Feed a payload where
|
|
5309
|
-
// failed_cap and the step's cap_urn differ only in tag order — they
|
|
5310
|
-
// should still match, proving URNs are not treated as strings.
|
|
5508
|
+
// TEST6506: Renderer run graph resolves a failure by exact stable step token
|
|
5509
|
+
function test6506_Renderer_buildRunGraphData_resolvesExactFailedStepToken() {
|
|
5311
5510
|
const strand = {
|
|
5312
5511
|
source_media_urn: 'media:a',
|
|
5313
5512
|
target_media_urn: 'media:c',
|
|
5314
|
-
steps: [
|
|
5513
|
+
steps: wireLinearStepInputs([
|
|
5315
5514
|
makeForEachStep('media:a;list'),
|
|
5316
5515
|
// Canonical form places tags alphabetically: op after in/out.
|
|
5317
5516
|
makeCapStep(
|
|
@@ -5323,11 +5522,8 @@ function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap(
|
|
|
5323
5522
|
'y', 'media:b', 'media:c', false, false
|
|
5324
5523
|
),
|
|
5325
5524
|
makeCollectStep('media:c'),
|
|
5326
|
-
],
|
|
5525
|
+
]),
|
|
5327
5526
|
};
|
|
5328
|
-
// The failed_cap URN is semantically the same as step 1 (cap y). If
|
|
5329
|
-
// CapUrn.fromString canonicalizes (it should), any equivalent form
|
|
5330
|
-
// will match. Feed a fully-specified form that is equivalent.
|
|
5331
5527
|
const payload = {
|
|
5332
5528
|
resolved_strand: strand,
|
|
5333
5529
|
media_display_names: {
|
|
@@ -5339,12 +5535,14 @@ function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap(
|
|
|
5339
5535
|
body_outcomes: [
|
|
5340
5536
|
{
|
|
5341
5537
|
body_index: 0,
|
|
5538
|
+
foreach_token_id: strand.steps[0].token_id,
|
|
5342
5539
|
success: false,
|
|
5343
5540
|
cap_urns: [],
|
|
5344
5541
|
saved_paths: [],
|
|
5345
5542
|
total_bytes: 0,
|
|
5346
5543
|
duration_ms: 0,
|
|
5347
|
-
|
|
5544
|
+
failed_token_id: strand.steps[2].token_id,
|
|
5545
|
+
failed_arg_urn: null,
|
|
5348
5546
|
error: 'fail',
|
|
5349
5547
|
},
|
|
5350
5548
|
],
|
|
@@ -5359,7 +5557,19 @@ function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap(
|
|
|
5359
5557
|
}
|
|
5360
5558
|
// 1 entry + 2 body step nodes (cap x and cap y, truncated
|
|
5361
5559
|
// at cap y) = 3 failure replica nodes.
|
|
5362
|
-
assertEqual(failureNodes, 3, 'trace truncates at cap y
|
|
5560
|
+
assertEqual(failureNodes, 3, 'trace truncates at cap y by stable token, yielding entry + 2 cap nodes');
|
|
5561
|
+
|
|
5562
|
+
const invalid = JSON.parse(JSON.stringify(payload));
|
|
5563
|
+
invalid.body_outcomes[0].failed_token_id = 'missing-step-token';
|
|
5564
|
+
let threw = false;
|
|
5565
|
+
try {
|
|
5566
|
+
rendererBuildRunGraphData(invalid);
|
|
5567
|
+
} catch (error) {
|
|
5568
|
+
threw = true;
|
|
5569
|
+
assert(error.message.includes('does not resolve to a strand step'),
|
|
5570
|
+
'unknown token failure must identify the unresolved coordinate');
|
|
5571
|
+
}
|
|
5572
|
+
assert(threw, 'unknown failed_token_id must fail hard');
|
|
5363
5573
|
}
|
|
5364
5574
|
|
|
5365
5575
|
// TEST6507: Renderer build run graph data backbone has no foreach node
|
|
@@ -5378,11 +5588,11 @@ function test6507_Renderer_buildRunGraphData_backboneHasNoForeachNode() {
|
|
|
5378
5588
|
const strand = {
|
|
5379
5589
|
source_media_urn: 'media:ext=pdf',
|
|
5380
5590
|
target_media_urn: 'media:decision',
|
|
5381
|
-
steps: [
|
|
5591
|
+
steps: wireLinearStepInputs([
|
|
5382
5592
|
makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
|
|
5383
5593
|
makeForEachStep('media:page'),
|
|
5384
5594
|
makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
5385
|
-
],
|
|
5595
|
+
]),
|
|
5386
5596
|
};
|
|
5387
5597
|
const payload = {
|
|
5388
5598
|
resolved_strand: strand,
|
|
@@ -5427,13 +5637,12 @@ function test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
|
|
|
5427
5637
|
const strand = {
|
|
5428
5638
|
source_media_urn: 'media:ext=pdf',
|
|
5429
5639
|
target_media_urn: 'media:decision',
|
|
5430
|
-
steps: [
|
|
5640
|
+
steps: wireLinearStepInputs([
|
|
5431
5641
|
makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
|
|
5432
5642
|
makeForEachStep('media:page'),
|
|
5433
5643
|
makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
5434
|
-
],
|
|
5644
|
+
]),
|
|
5435
5645
|
};
|
|
5436
|
-
const failedCapUrn = 'cap:in="media:page";decide;out="media:decision"';
|
|
5437
5646
|
const payload = {
|
|
5438
5647
|
resolved_strand: strand,
|
|
5439
5648
|
media_display_names: {
|
|
@@ -5442,8 +5651,8 @@ function test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
|
|
|
5442
5651
|
'media:decision': 'Decision',
|
|
5443
5652
|
},
|
|
5444
5653
|
body_outcomes: [
|
|
5445
|
-
{ body_index: 0, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0,
|
|
5446
|
-
{ body_index: 1, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0,
|
|
5654
|
+
{ body_index: 0, foreach_token_id: strand.steps[1].token_id, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: strand.steps[2].token_id, failed_arg_urn: null, error: 'boom' },
|
|
5655
|
+
{ body_index: 1, foreach_token_id: strand.steps[1].token_id, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: strand.steps[2].token_id, failed_arg_urn: null, error: 'boom' },
|
|
5447
5656
|
],
|
|
5448
5657
|
visible_success_count: 3,
|
|
5449
5658
|
visible_failure_count: 3,
|
|
@@ -5498,11 +5707,11 @@ function test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
|
|
|
5498
5707
|
const strand = {
|
|
5499
5708
|
source_media_urn: 'media:ext=pdf',
|
|
5500
5709
|
target_media_urn: 'media:decision',
|
|
5501
|
-
steps: [
|
|
5710
|
+
steps: wireLinearStepInputs([
|
|
5502
5711
|
makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
|
|
5503
5712
|
makeForEachStep('media:page'),
|
|
5504
5713
|
makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
5505
|
-
],
|
|
5714
|
+
]),
|
|
5506
5715
|
};
|
|
5507
5716
|
const payload = {
|
|
5508
5717
|
resolved_strand: strand,
|
|
@@ -5512,7 +5721,7 @@ function test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
|
|
|
5512
5721
|
'media:decision': 'Decision',
|
|
5513
5722
|
},
|
|
5514
5723
|
body_outcomes: [
|
|
5515
|
-
{ body_index: 0, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0 },
|
|
5724
|
+
{ body_index: 0, foreach_token_id: strand.steps[1].token_id, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: null, failed_arg_urn: null },
|
|
5516
5725
|
],
|
|
5517
5726
|
visible_success_count: 3,
|
|
5518
5727
|
visible_failure_count: 3,
|
|
@@ -5558,11 +5767,11 @@ function test6510_Renderer_buildRunGraphData_closedForeachSuccessMergesAtCollect
|
|
|
5558
5767
|
const strand = {
|
|
5559
5768
|
source_media_urn: 'media:ext=pdf;list',
|
|
5560
5769
|
target_media_urn: 'media:txt;list',
|
|
5561
|
-
steps: [
|
|
5770
|
+
steps: wireLinearStepInputs([
|
|
5562
5771
|
makeForEachStep('media:ext=pdf;list'),
|
|
5563
5772
|
makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
|
|
5564
5773
|
makeCollectStep('media:ext=txt'),
|
|
5565
|
-
],
|
|
5774
|
+
]),
|
|
5566
5775
|
};
|
|
5567
5776
|
const payload = {
|
|
5568
5777
|
resolved_strand: strand,
|
|
@@ -5573,7 +5782,7 @@ function test6510_Renderer_buildRunGraphData_closedForeachSuccessMergesAtCollect
|
|
|
5573
5782
|
'media:txt;list': 'Text List',
|
|
5574
5783
|
},
|
|
5575
5784
|
body_outcomes: [
|
|
5576
|
-
{ body_index: 0, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0 },
|
|
5785
|
+
{ body_index: 0, foreach_token_id: strand.steps[0].token_id, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: null, failed_arg_urn: null },
|
|
5577
5786
|
],
|
|
5578
5787
|
visible_success_count: 3,
|
|
5579
5788
|
visible_failure_count: 3,
|
|
@@ -6337,7 +6546,7 @@ function test6736_axisWeightingDecodedLayout() {
|
|
|
6337
6546
|
// TEST6737: Cap with version=0 round-trips with no `version` key on wire
|
|
6338
6547
|
function test6737_capVersionZeroOmittedOnWire() {
|
|
6339
6548
|
const urn = CapUrn.fromString('cap:in="media:void";test-op;out="media:enc=utf-8;record"');
|
|
6340
|
-
const cap = new Cap(urn, 'Test Cap', 'test-op');
|
|
6549
|
+
const cap = new Cap(urn, 'Test Cap', ['test-op']);
|
|
6341
6550
|
// version defaults to 0
|
|
6342
6551
|
assertEqual(cap.version, 0, 'Default version should be 0');
|
|
6343
6552
|
const json = cap.toJSON();
|
|
@@ -6349,7 +6558,7 @@ function test6737_capVersionZeroOmittedOnWire() {
|
|
|
6349
6558
|
// TEST1848: Cap with version=N round-trips with `version: N` on wire
|
|
6350
6559
|
function test1848_capVersionNonZeroOnWire() {
|
|
6351
6560
|
const urn = CapUrn.fromString('cap:in="media:void";versioned-op;out="media:enc=utf-8;record"');
|
|
6352
|
-
const cap = new Cap(urn, 'Versioned Cap', 'versioned-op');
|
|
6561
|
+
const cap = new Cap(urn, 'Versioned Cap', ['versioned-op']);
|
|
6353
6562
|
cap.version = 42;
|
|
6354
6563
|
const json = cap.toJSON();
|
|
6355
6564
|
assert('version' in json, 'version!=0 must appear on wire');
|
|
@@ -6358,6 +6567,139 @@ function test1848_capVersionNonZeroOnWire() {
|
|
|
6358
6567
|
assertEqual(restored.version, 42, 'Restored version must equal 42');
|
|
6359
6568
|
}
|
|
6360
6569
|
|
|
6570
|
+
// ===========================================================================
|
|
6571
|
+
// Shared parity tests 7100-7104: CapArg.streamUrn / CapArg.isMainInput.
|
|
6572
|
+
// Same substantive assertions in every capdag mirror (rust, go, js, objc, py).
|
|
6573
|
+
// ===========================================================================
|
|
6574
|
+
|
|
6575
|
+
// TEST7100: streamUrn() returns the stdin source's URN when it differs from the declared slot media_urn — the stdin URN, not the slot URN, is what the runtime demuxes the arg's input stream by.
|
|
6576
|
+
function test7100_streamUrnReturnsStdinSourceUrnWhenItDiffersFromSlotUrn() {
|
|
6577
|
+
const arg = new CapArg(
|
|
6578
|
+
'media:enc=utf-8;file-path',
|
|
6579
|
+
true,
|
|
6580
|
+
[new ArgSource({ stdin: 'media:ext=pdf;pdf-stream' })]
|
|
6581
|
+
);
|
|
6582
|
+
assertEqual(arg.streamUrn(), 'media:ext=pdf;pdf-stream',
|
|
6583
|
+
'streamUrn must return the stdin source URN');
|
|
6584
|
+
assert(arg.streamUrn() !== arg.media_urn,
|
|
6585
|
+
'stream URN must differ from the declared slot media_urn here');
|
|
6586
|
+
}
|
|
6587
|
+
|
|
6588
|
+
// TEST7101: streamUrn() falls back to the declared slot media_urn when the arg declares no stdin source — a producer-fed arg may be delivered by its declared URN without ever appearing on stdin.
|
|
6589
|
+
function test7101_streamUrnFallsBackToDeclaredMediaUrnWithoutStdinSource() {
|
|
6590
|
+
const arg = new CapArg(
|
|
6591
|
+
'media:enc=utf-8;system-prompt',
|
|
6592
|
+
true,
|
|
6593
|
+
[new ArgSource({ cli_flag: '--system-prompt' })]
|
|
6594
|
+
);
|
|
6595
|
+
assertEqual(arg.streamUrn(), 'media:enc=utf-8;system-prompt',
|
|
6596
|
+
'streamUrn must fall back to the declared media_urn');
|
|
6597
|
+
}
|
|
6598
|
+
|
|
6599
|
+
// TEST7102: isMainInput() is true when the stdin URN is order-theoretically EQUIVALENT to the cap's in= spec even when the two strings list their tags in a different order — the comparison is the MediaUrn equivalence predicate, never a string comparison.
|
|
6600
|
+
function test7102_isMainInputTrueOnTagOrderInsensitiveEquivalenceToInSpec() {
|
|
6601
|
+
const inSpec = MediaUrn.fromString('media:ext=pdf;pdf-stream');
|
|
6602
|
+
const arg = new CapArg(
|
|
6603
|
+
'media:enc=utf-8;file-path',
|
|
6604
|
+
true,
|
|
6605
|
+
[new ArgSource({ stdin: 'media:pdf-stream;ext=pdf' })] // same tags, different order
|
|
6606
|
+
);
|
|
6607
|
+
assert(arg.isMainInput(inSpec),
|
|
6608
|
+
'tag-order-insensitive equivalent stdin URN must be the main input');
|
|
6609
|
+
// The raw strings genuinely differ — proves the match is equivalence,
|
|
6610
|
+
// not string equality.
|
|
6611
|
+
assert(inSpec.toString() !== 'media:pdf-stream;ext=pdf',
|
|
6612
|
+
'raw spec strings must differ for this test to be meaningful');
|
|
6613
|
+
}
|
|
6614
|
+
|
|
6615
|
+
// TEST7103: isMainInput() is false for cli_flag-only and position-only args (no stdin source means never the main input, whatever the declared slot URN says), and false when the stdin URN is not equivalent to in=.
|
|
6616
|
+
function test7103_isMainInputFalseWithoutEquivalentStdinSource() {
|
|
6617
|
+
const inSpec = MediaUrn.fromString('media:ext=pdf;pdf-stream');
|
|
6618
|
+
|
|
6619
|
+
const cliFlagOnly = new CapArg(
|
|
6620
|
+
'media:ext=pdf;pdf-stream', // slot URN even matches in= — irrelevant
|
|
6621
|
+
true,
|
|
6622
|
+
[new ArgSource({ cli_flag: '--input' })]
|
|
6623
|
+
);
|
|
6624
|
+
assert(!cliFlagOnly.isMainInput(inSpec), 'cli_flag-only arg is never the main input');
|
|
6625
|
+
|
|
6626
|
+
const positionOnly = new CapArg(
|
|
6627
|
+
'media:ext=pdf;pdf-stream',
|
|
6628
|
+
true,
|
|
6629
|
+
[new ArgSource({ position: 0 })]
|
|
6630
|
+
);
|
|
6631
|
+
assert(!positionOnly.isMainInput(inSpec), 'position-only arg is never the main input');
|
|
6632
|
+
|
|
6633
|
+
const nonEquivalentStdin = new CapArg(
|
|
6634
|
+
'media:enc=utf-8;system-prompt',
|
|
6635
|
+
false,
|
|
6636
|
+
[new ArgSource({ stdin: 'media:enc=utf-8;system-prompt' })]
|
|
6637
|
+
);
|
|
6638
|
+
assert(!nonEquivalentStdin.isMainInput(inSpec),
|
|
6639
|
+
'stdin URN not equivalent to in= must not be the main input');
|
|
6640
|
+
}
|
|
6641
|
+
|
|
6642
|
+
// TEST7104: A realistic multi-arg cap (one stdin main input; one required, defaultless cli_flag arg; several defaulted cli_flag args): exactly one arg is the main input, and partitioning the remaining args by required-without-default vs has-default yields the expected sets.
|
|
6643
|
+
function test7104_multiArgCapExactlyOneMainInputAndPartitionOfRest() {
|
|
6644
|
+
const inSpec = MediaUrn.fromString('media:ext=pdf;pdf-stream');
|
|
6645
|
+
|
|
6646
|
+
const args = [
|
|
6647
|
+
new CapArg(
|
|
6648
|
+
'media:enc=utf-8;file-path',
|
|
6649
|
+
true,
|
|
6650
|
+
// Main input may ALSO be delivered by cli-flag; stdin is the
|
|
6651
|
+
// defining route.
|
|
6652
|
+
[new ArgSource({ stdin: 'media:pdf-stream;ext=pdf' }), new ArgSource({ cli_flag: '--input' })]
|
|
6653
|
+
),
|
|
6654
|
+
new CapArg(
|
|
6655
|
+
'media:enc=utf-8;question',
|
|
6656
|
+
true,
|
|
6657
|
+
[new ArgSource({ cli_flag: '--question' })]
|
|
6658
|
+
),
|
|
6659
|
+
new CapArg(
|
|
6660
|
+
'media:max-tokens;numeric',
|
|
6661
|
+
false,
|
|
6662
|
+
[new ArgSource({ cli_flag: '--max-tokens' })],
|
|
6663
|
+
{ default_value: 1024 }
|
|
6664
|
+
),
|
|
6665
|
+
new CapArg(
|
|
6666
|
+
'media:numeric;temperature',
|
|
6667
|
+
false,
|
|
6668
|
+
[new ArgSource({ cli_flag: '--temperature' })],
|
|
6669
|
+
{ default_value: 0.7 }
|
|
6670
|
+
),
|
|
6671
|
+
new CapArg(
|
|
6672
|
+
'media:enc=utf-8;system-prompt',
|
|
6673
|
+
false,
|
|
6674
|
+
[new ArgSource({ cli_flag: '--system-prompt' })],
|
|
6675
|
+
{ default_value: 'You are a helpful assistant.' }
|
|
6676
|
+
),
|
|
6677
|
+
];
|
|
6678
|
+
|
|
6679
|
+
const mainInputs = args.filter(a => a.isMainInput(inSpec)).map(a => a.media_urn);
|
|
6680
|
+
const rest = args.filter(a => !a.isMainInput(inSpec));
|
|
6681
|
+
const requiredWithoutDefault = rest
|
|
6682
|
+
.filter(a => a.required && a.default_value === null)
|
|
6683
|
+
.map(a => a.media_urn);
|
|
6684
|
+
const withDefault = rest
|
|
6685
|
+
.filter(a => a.default_value !== null)
|
|
6686
|
+
.map(a => a.media_urn);
|
|
6687
|
+
|
|
6688
|
+
assertEqual(JSON.stringify(mainInputs),
|
|
6689
|
+
JSON.stringify(['media:enc=utf-8;file-path']),
|
|
6690
|
+
'exactly one arg must be the main input');
|
|
6691
|
+
assertEqual(JSON.stringify(requiredWithoutDefault),
|
|
6692
|
+
JSON.stringify(['media:enc=utf-8;question']),
|
|
6693
|
+
'required-without-default partition');
|
|
6694
|
+
assertEqual(JSON.stringify(withDefault),
|
|
6695
|
+
JSON.stringify([
|
|
6696
|
+
'media:max-tokens;numeric',
|
|
6697
|
+
'media:numeric;temperature',
|
|
6698
|
+
'media:enc=utf-8;system-prompt',
|
|
6699
|
+
]),
|
|
6700
|
+
'has-default partition');
|
|
6701
|
+
}
|
|
6702
|
+
|
|
6361
6703
|
// ===========================================================================
|
|
6362
6704
|
// Fabric alias tests (shared numbers 1880-1882, 1887)
|
|
6363
6705
|
//
|
|
@@ -6519,6 +6861,167 @@ function test1196_aliasedSerializationUsesAliasAndDropsHeader() {
|
|
|
6519
6861
|
// Test runner
|
|
6520
6862
|
// ============================================================================
|
|
6521
6863
|
|
|
6864
|
+
|
|
6865
|
+
// ============================================================================
|
|
6866
|
+
// Unified planner plan-space (planner.js) — TEST1450+
|
|
6867
|
+
// ============================================================================
|
|
6868
|
+
|
|
6869
|
+
const plannerNS = require('./planner.js');
|
|
6870
|
+
|
|
6871
|
+
/** Planner-specific throw assertion: any PlanStateError counts. */
|
|
6872
|
+
function assertThrowsPlanState(fn, message) {
|
|
6873
|
+
try {
|
|
6874
|
+
fn();
|
|
6875
|
+
} catch (error) {
|
|
6876
|
+
if (error instanceof plannerNS.PlanStateError) return;
|
|
6877
|
+
throw new Error(`Expected PlanStateError but got ${error.name}: ${error.message} (${message})`);
|
|
6878
|
+
}
|
|
6879
|
+
throw new Error(`Expected PlanStateError but function succeeded: ${message}`);
|
|
6880
|
+
}
|
|
6881
|
+
|
|
6882
|
+
// TEST1450: a defaults-only request is AUTO with every knob at its default,
|
|
6883
|
+
// and the proto JSON carries the documented wire shape.
|
|
6884
|
+
function test1450_planRequestDefaults() {
|
|
6885
|
+
const request = new plannerNS.PlanRequest({
|
|
6886
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6887
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6888
|
+
});
|
|
6889
|
+
assertEqual(request.isConfigured, false, 'defaults are not configured');
|
|
6890
|
+
const wire = request.toProtoJSON();
|
|
6891
|
+
assertEqual(wire.mode, 0, 'AUTO mode');
|
|
6892
|
+
assertEqual(wire.ranking, 0, 'intent ranking');
|
|
6893
|
+
assertEqual(wire.convergence.presence, 0, 'auto presence');
|
|
6894
|
+
assertEqual(wire.convergence.mechanism, 0, 'any mechanism');
|
|
6895
|
+
assertEqual(wire.sources[0].media_urn, 'media:ext=pdf', 'snake_case source field');
|
|
6896
|
+
assertEqual(wire.sources[0].is_sequence, false, 'snake_case sequence field');
|
|
6897
|
+
assertEqual(wire.max_depth, 0, '0 = engine default depth');
|
|
6898
|
+
}
|
|
6899
|
+
|
|
6900
|
+
// TEST1451: at-depth REQUIRES a positive depth; a depth anywhere else is
|
|
6901
|
+
// invalid; ranking never flips the request to CONFIGURED but any
|
|
6902
|
+
// space-constraining knob does (mode is forced to CONFIGURED on the wire).
|
|
6903
|
+
function test1451_planRequestKnobValidation() {
|
|
6904
|
+
assertThrowsPlanState(() => new plannerNS.PlanRequest({
|
|
6905
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6906
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6907
|
+
convergenceLocation: 'at-depth',
|
|
6908
|
+
}), 'at-depth without a depth must throw');
|
|
6909
|
+
|
|
6910
|
+
assertThrowsPlanState(() => new plannerNS.PlanRequest({
|
|
6911
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6912
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6913
|
+
convergenceAtDepth: 2,
|
|
6914
|
+
}), 'a depth without at-depth must throw');
|
|
6915
|
+
|
|
6916
|
+
const ranked = new plannerNS.PlanRequest({
|
|
6917
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6918
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6919
|
+
ranking: 'shortest',
|
|
6920
|
+
});
|
|
6921
|
+
assertEqual(ranked.isConfigured, false, 'ranking alone never configures');
|
|
6922
|
+
assertEqual(ranked.toProtoJSON().mode, 0, 'ranking alone stays AUTO');
|
|
6923
|
+
|
|
6924
|
+
const constrained = new plannerNS.PlanRequest({
|
|
6925
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6926
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6927
|
+
convergenceMechanism: 'collect',
|
|
6928
|
+
});
|
|
6929
|
+
assertEqual(constrained.isConfigured, true, 'a mechanism constrains');
|
|
6930
|
+
assertEqual(constrained.toProtoJSON().mode, 1, 'constrained ⇒ CONFIGURED on the wire');
|
|
6931
|
+
|
|
6932
|
+
assertThrowsPlanState(() => new plannerNS.PlanRequest({
|
|
6933
|
+
sources: [],
|
|
6934
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6935
|
+
}), 'no sources must throw');
|
|
6936
|
+
assertThrowsPlanState(() => new plannerNS.PlanRequest({
|
|
6937
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6938
|
+
exactTargets: [],
|
|
6939
|
+
}), 'no targets must throw');
|
|
6940
|
+
}
|
|
6941
|
+
|
|
6942
|
+
// TEST1452: every knob round-trips value → proto number → value, and an
|
|
6943
|
+
// unknown value/number fails hard in both directions.
|
|
6944
|
+
function test1452_knobProtoRoundTrip() {
|
|
6945
|
+
const knobs = [
|
|
6946
|
+
plannerNS.ConvergencePresence, plannerNS.ConvergenceLocation,
|
|
6947
|
+
plannerNS.ConvergenceMechanism, plannerNS.ConvergenceArity,
|
|
6948
|
+
plannerNS.DivergencePresence, plannerNS.DivergenceLocation,
|
|
6949
|
+
plannerNS.RankPolicy, plannerNS.PlanMode,
|
|
6950
|
+
];
|
|
6951
|
+
for (const knob of knobs) {
|
|
6952
|
+
for (const value of knob.values) {
|
|
6953
|
+
assertEqual(knob.fromProto(knob.toProto(value)), value, `${knob.name} round-trips ${value}`);
|
|
6954
|
+
}
|
|
6955
|
+
assertThrowsPlanState(() => knob.toProto('no-such-value'), `${knob.name} rejects unknown value`);
|
|
6956
|
+
assertThrowsPlanState(() => knob.fromProto(999), `${knob.name} rejects unknown number`);
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
|
|
6960
|
+
// TEST1453: candidate/target parsers accept realistic proto-JSON and sort
|
|
6961
|
+
// candidates by rank; malformed payloads (missing field, wrong type) fail
|
|
6962
|
+
// hard with the offending path in the message.
|
|
6963
|
+
function test1453_planResponseParsers() {
|
|
6964
|
+
const candidates = plannerNS.parsePlanCandidates({
|
|
6965
|
+
candidates: [
|
|
6966
|
+
{
|
|
6967
|
+
notation: '[n0 -> edge_1 -> n1]',
|
|
6968
|
+
label: 'Convert each',
|
|
6969
|
+
rank: 1,
|
|
6970
|
+
profile: { source_media: ['media:ext=pdf'], target_media: ['media:enc=utf-8;ext=txt'], apexes: [], converged: false, diverged: false },
|
|
6971
|
+
cost: { cap_steps: 2, total_steps: 2, max_leg_depth: 0, intent_score: 0.5 },
|
|
6972
|
+
},
|
|
6973
|
+
{
|
|
6974
|
+
notation: '[(n0, n1) -> edge_0 -> n2]',
|
|
6975
|
+
label: 'Combine via text',
|
|
6976
|
+
rank: 0,
|
|
6977
|
+
profile: {
|
|
6978
|
+
source_media: ['media:ext=pdf', 'media:ext=md'],
|
|
6979
|
+
target_media: ['media:enc=utf-8;ext=txt'],
|
|
6980
|
+
apexes: [{ media_urn: 'media:enc=utf-8;page', mechanism: 2, depth: 1 }],
|
|
6981
|
+
converged: true,
|
|
6982
|
+
diverged: false,
|
|
6983
|
+
},
|
|
6984
|
+
cost: { cap_steps: 3, total_steps: 4, max_leg_depth: 1, intent_score: 0.8 },
|
|
6985
|
+
},
|
|
6986
|
+
],
|
|
6987
|
+
});
|
|
6988
|
+
assertEqual(candidates.length, 2, 'two candidates');
|
|
6989
|
+
assertEqual(candidates[0].rank, 0, 'sorted by rank');
|
|
6990
|
+
assertEqual(candidates[0].profile.apexes[0].mechanism, 'collect', 'proto mechanism decoded to knob value');
|
|
6991
|
+
assertEqual(candidates[0].profile.converged, true, 'converged carried');
|
|
6992
|
+
|
|
6993
|
+
assertThrowsPlanState(
|
|
6994
|
+
() => plannerNS.parsePlanCandidates({ candidates: [{ label: 'missing notation', rank: 0 }] }),
|
|
6995
|
+
'a candidate without notation must fail hard'
|
|
6996
|
+
);
|
|
6997
|
+
|
|
6998
|
+
const targets = plannerNS.parseConvergentTargets({
|
|
6999
|
+
targets: [
|
|
7000
|
+
{
|
|
7001
|
+
media_def: 'media:enc=utf-8;ext=txt',
|
|
7002
|
+
display_name: 'Plain Text',
|
|
7003
|
+
min_total_steps: 3,
|
|
7004
|
+
apex: { media_urn: 'media:enc=utf-8;page', mechanism: 2, depth: 1 },
|
|
7005
|
+
convergent: true,
|
|
7006
|
+
file_extension: 'txt',
|
|
7007
|
+
},
|
|
7008
|
+
{
|
|
7009
|
+
media_def: 'media:ext=png;image',
|
|
7010
|
+
display_name: 'PNG Image',
|
|
7011
|
+
min_total_steps: 2,
|
|
7012
|
+
convergent: false,
|
|
7013
|
+
file_extension: 'png',
|
|
7014
|
+
},
|
|
7015
|
+
],
|
|
7016
|
+
});
|
|
7017
|
+
assertEqual(targets[0].apex.mediaUrn, 'media:enc=utf-8;page', 'apex carried');
|
|
7018
|
+
assertEqual(targets[1].apex, null, 'independent target has no apex');
|
|
7019
|
+
assertThrowsPlanState(
|
|
7020
|
+
() => plannerNS.parseConvergentTargets({ targets: [{ display_name: 'no media_def' }] }),
|
|
7021
|
+
'a target without media_def must fail hard'
|
|
7022
|
+
);
|
|
7023
|
+
}
|
|
7024
|
+
|
|
6522
7025
|
async function runTests() {
|
|
6523
7026
|
console.log('Running capdag-js tests...\n');
|
|
6524
7027
|
|
|
@@ -6721,7 +7224,7 @@ async function runTests() {
|
|
|
6721
7224
|
await runTest('TEST1875: scan_all_reaches_both_dev_and_registry_slugs', test1875_scanAllReachesBothDevAndRegistrySlugs);
|
|
6722
7225
|
await runTest('TEST1876: other_channel_subtree_is_skipped', test1876_otherChannelSubtreeIsSkipped);
|
|
6723
7226
|
await runTest('TEST1877: registry_cartridge_under_wrong_slug_is_bad_install', test1877_registryCartridgeUnderWrongSlugIsBadInstall);
|
|
6724
|
-
await runTest('TEST1878:
|
|
7227
|
+
await runTest('TEST1878: bundled_cartridge_without_baked_hash_is_rejected', test1878_bundledCartridgeWithoutBakedHashIsRejected);
|
|
6725
7228
|
|
|
6726
7229
|
// media_urn.rs: TEST1312-TEST1315, TEST1298-TEST1302 (MediaUrn predicates)
|
|
6727
7230
|
console.log('\n--- media_urn.rs (predicates) ---');
|
|
@@ -6912,12 +7415,14 @@ async function runTests() {
|
|
|
6912
7415
|
runTest('RENDERER: collapseStrand_plainCapMergesOutput', test6500_Renderer_collapseStrand_plainCapMergesTrailingOutput);
|
|
6913
7416
|
runTest('RENDERER: collapseStrand_plainCapDistinctTarget', test6501_Renderer_collapseStrand_plainCapDistinctTargetNoMerge);
|
|
6914
7417
|
runTest('RENDERER: validateStrand_missingSourceMediaUrn', test6502_Renderer_validateStrandPayload_missingSourceMediaUrn);
|
|
7418
|
+
runTest('RENDERER: validateStrand_requiresCapInputs', test7120_Renderer_validateStrandPayload_requiresAuthoritativeInputs);
|
|
6915
7419
|
|
|
6916
7420
|
console.log('\n--- cap-fab-renderer run builder ---');
|
|
6917
7421
|
runTest('RENDERER: validateBodyOutcome_negativeIndex', test6503_Renderer_validateBodyOutcome_rejectsNegativeIndex);
|
|
7422
|
+
runTest('RENDERER: validateBodyOutcome_failureCoordinate', test7121_Renderer_validateBodyOutcome_requiresExplicitFailureCoordinate);
|
|
6918
7423
|
runTest('RENDERER: buildRun_pagesSuccessesAndFailures', test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures);
|
|
6919
|
-
runTest('RENDERER:
|
|
6920
|
-
runTest('RENDERER:
|
|
7424
|
+
runTest('RENDERER: buildRun_unattributedFailureAtEntry', test6505_Renderer_buildRunGraphData_unattributedFailureStopsAtEntry);
|
|
7425
|
+
runTest('RENDERER: buildRun_resolvesFailedStepToken', test6506_Renderer_buildRunGraphData_resolvesExactFailedStepToken);
|
|
6921
7426
|
runTest('RENDERER: buildRun_backboneHasNoForeachNode', test6507_Renderer_buildRunGraphData_backboneHasNoForeachNode);
|
|
6922
7427
|
runTest('RENDERER: buildRun_allFailedDropsPlaceholder', test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder);
|
|
6923
7428
|
runTest('RENDERER: buildRun_unclosedForeachNoMerge', test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge);
|
|
@@ -6976,6 +7481,13 @@ async function runTests() {
|
|
|
6976
7481
|
runTest('TEST1847: cap_version_zero_omitted_on_wire', test6737_capVersionZeroOmittedOnWire);
|
|
6977
7482
|
runTest('TEST1848: cap_version_nonzero_on_wire', test1848_capVersionNonZeroOnWire);
|
|
6978
7483
|
|
|
7484
|
+
console.log('\n--- CapArg stream URN + main input (test7100–test7104) ---');
|
|
7485
|
+
runTest('TEST7100: stream_urn_prefers_stdin_source_urn', test7100_streamUrnReturnsStdinSourceUrnWhenItDiffersFromSlotUrn);
|
|
7486
|
+
runTest('TEST7101: stream_urn_falls_back_to_media_urn', test7101_streamUrnFallsBackToDeclaredMediaUrnWithoutStdinSource);
|
|
7487
|
+
runTest('TEST7102: is_main_input_by_urn_equivalence', test7102_isMainInputTrueOnTagOrderInsensitiveEquivalenceToInSpec);
|
|
7488
|
+
runTest('TEST7103: is_main_input_false_without_stdin', test7103_isMainInputFalseWithoutEquivalentStdinSource);
|
|
7489
|
+
runTest('TEST7104: multi_arg_main_input_partition', test7104_multiArgCapExactlyOneMainInputAndPartitionOfRest);
|
|
7490
|
+
|
|
6979
7491
|
runTest('TEST1880: alias_name_normalization_rules', test1880_aliasNameNormalizationRules);
|
|
6980
7492
|
runTest('TEST1881: token_urn_vs_alias_detection', test1881_tokenUrnVsAliasDetection);
|
|
6981
7493
|
runTest('TEST1882: classify_alias_target_by_prefix', test1882_classifyAliasTargetByPrefix);
|
|
@@ -6987,6 +7499,13 @@ async function runTests() {
|
|
|
6987
7499
|
runTest('TEST1896: cached_cap_aliases_filters_to_cap', test1896_cachedCapAliasesFiltersToCapTargets);
|
|
6988
7500
|
runTest('TEST1196: aliased_serialization_uses_alias', test1196_aliasedSerializationUsesAliasAndDropsHeader);
|
|
6989
7501
|
|
|
7502
|
+
// planner.js: unified plan-space vocabulary
|
|
7503
|
+
console.log('--- planner.js ---');
|
|
7504
|
+
runTest('TEST1450: plan_request_defaults', test1450_planRequestDefaults);
|
|
7505
|
+
runTest('TEST1451: plan_request_knob_validation', test1451_planRequestKnobValidation);
|
|
7506
|
+
runTest('TEST1452: knob_proto_round_trip', test1452_knobProtoRoundTrip);
|
|
7507
|
+
runTest('TEST1453: plan_response_parsers', test1453_planResponseParsers);
|
|
7508
|
+
|
|
6990
7509
|
// Summary
|
|
6991
7510
|
console.log(`\n${passCount + failCount} tests: ${passCount} passed, ${failCount} failed`);
|
|
6992
7511
|
if (failCount > 0) {
|