capdag 1.233.649 → 1.241.684
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 +125 -60
- package/capdag.js +87 -27
- package/capdag.test.js +430 -83
- package/package.json +2 -1
- package/planner.js +424 -0
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();
|
|
@@ -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(
|
|
@@ -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
|
|
|
@@ -2687,11 +2700,11 @@ async function test1877_registryCartridgeUnderWrongSlugIsBadInstall() {
|
|
|
2687
2700
|
}
|
|
2688
2701
|
|
|
2689
2702
|
// TEST1878: a cartridge marked `installed_from: bundle` with no baked hash in
|
|
2690
|
-
//
|
|
2703
|
+
// BUNDLED_CARTRIDGE_HASHES (empty in this mirror) is rejected as BadInstallation
|
|
2691
2704
|
// — the bundled-integrity gate fires before the probe. Non-macOS only: on macOS
|
|
2692
2705
|
// the baked-hash path is intentionally absent (OS code-signature is the guard),
|
|
2693
|
-
// so a bundled
|
|
2694
|
-
async function
|
|
2706
|
+
// so a bundled cartridge is accepted there and would instead end at the probe.
|
|
2707
|
+
async function test1878_bundledCartridgeWithoutBakedHashIsRejected() {
|
|
2695
2708
|
if (process.platform === 'darwin') {
|
|
2696
2709
|
// Mirrors the Rust #[cfg(not(target_os = "macos"))] gate: on macOS the
|
|
2697
2710
|
// baked-hash path does not exist, so this scenario is not exercised.
|
|
@@ -2709,7 +2722,7 @@ async function test1878_bundledProviderWithoutBakedHashIsRejected() {
|
|
|
2709
2722
|
discoveryInstallFixture(root, 'dev', 'nightly', 'cart', '1.0.0', json, 'cart');
|
|
2710
2723
|
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
|
|
2711
2724
|
discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.BAD_INSTALLATION);
|
|
2712
|
-
assert(out[0].error.message.includes('bundled
|
|
2725
|
+
assert(out[0].error.message.includes('bundled cartridge integrity'),
|
|
2713
2726
|
`message should name the bundled-integrity failure: ${out[0].error.message}`);
|
|
2714
2727
|
} finally {
|
|
2715
2728
|
discoveryRemoveRoot(root);
|
|
@@ -3195,11 +3208,11 @@ function test127_invalidEffectNoneFailsHard() {
|
|
|
3195
3208
|
|
|
3196
3209
|
// TEST128: omitted effect means declared; unconstrained effect must be explicit
|
|
3197
3210
|
function test128_effectDispatchRequiresExplicitWildcard() {
|
|
3198
|
-
const
|
|
3211
|
+
const noneCandidate = CapUrn.fromString('cap:effect=none');
|
|
3199
3212
|
const declaredRequest = CapUrn.fromString('cap:raw');
|
|
3200
3213
|
const anyRequest = CapUrn.fromString('cap:?effect');
|
|
3201
|
-
assert(!
|
|
3202
|
-
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');
|
|
3203
3216
|
}
|
|
3204
3217
|
|
|
3205
3218
|
// ============================================================================
|
|
@@ -4296,7 +4309,7 @@ function test6467_Machine_capRegistryEntry_construction() {
|
|
|
4296
4309
|
command: 'extract',
|
|
4297
4310
|
cap_description: 'Extracts text from PDF',
|
|
4298
4311
|
args: [{ media_urn: 'media:ext=pdf', required: true }],
|
|
4299
|
-
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' },
|
|
4300
4313
|
media_defs: [],
|
|
4301
4314
|
urn_tags: { op: 'extract' },
|
|
4302
4315
|
in_spec: 'media:ext=pdf',
|
|
@@ -4567,8 +4580,16 @@ function test6483_Renderer_buildBrowseGraphData_rejectsMissingMediaTitles() {
|
|
|
4567
4580
|
|
|
4568
4581
|
// ---------------- strand builder ----------------
|
|
4569
4582
|
|
|
4583
|
+
let fixtureStepToken = 0;
|
|
4584
|
+
|
|
4585
|
+
function nextFixtureStepToken() {
|
|
4586
|
+
fixtureStepToken += 1;
|
|
4587
|
+
return `fixture-step-${fixtureStepToken}`;
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4570
4590
|
function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
|
|
4571
4591
|
return {
|
|
4592
|
+
token_id: nextFixtureStepToken(),
|
|
4572
4593
|
step_type: {
|
|
4573
4594
|
Cap: {
|
|
4574
4595
|
cap_urn: capUrn,
|
|
@@ -4576,6 +4597,7 @@ function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
|
|
|
4576
4597
|
specificity: 0,
|
|
4577
4598
|
input_is_sequence: inSeq,
|
|
4578
4599
|
output_is_sequence: outSeq,
|
|
4600
|
+
inputs: [{ arg_urn: fromSpec, source: 'StrandInput' }],
|
|
4579
4601
|
},
|
|
4580
4602
|
},
|
|
4581
4603
|
from_spec: fromSpec,
|
|
@@ -4585,6 +4607,7 @@ function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
|
|
|
4585
4607
|
|
|
4586
4608
|
function makeForEachStep(mediaDef) {
|
|
4587
4609
|
return {
|
|
4610
|
+
token_id: nextFixtureStepToken(),
|
|
4588
4611
|
step_type: { ForEach: { media_def: mediaDef } },
|
|
4589
4612
|
from_spec: mediaDef,
|
|
4590
4613
|
to_spec: mediaDef,
|
|
@@ -4593,12 +4616,34 @@ function makeForEachStep(mediaDef) {
|
|
|
4593
4616
|
|
|
4594
4617
|
function makeCollectStep(mediaDef) {
|
|
4595
4618
|
return {
|
|
4619
|
+
token_id: nextFixtureStepToken(),
|
|
4596
4620
|
step_type: { Collect: { media_def: mediaDef } },
|
|
4597
4621
|
from_spec: mediaDef,
|
|
4598
4622
|
to_spec: mediaDef,
|
|
4599
4623
|
};
|
|
4600
4624
|
}
|
|
4601
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
|
+
|
|
4602
4647
|
// TEST6484: Renderer validate strand step rejects unknown variant
|
|
4603
4648
|
function test6484_Renderer_validateStrandStep_rejectsUnknownVariant() {
|
|
4604
4649
|
// A step with an unknown variant must fail hard at validation; no
|
|
@@ -4606,6 +4651,7 @@ function test6484_Renderer_validateStrandStep_rejectsUnknownVariant() {
|
|
|
4606
4651
|
let threw = false;
|
|
4607
4652
|
try {
|
|
4608
4653
|
rendererValidateStrandStep({
|
|
4654
|
+
token_id: nextFixtureStepToken(),
|
|
4609
4655
|
step_type: { WrongVariant: {} },
|
|
4610
4656
|
from_spec: 'media:a',
|
|
4611
4657
|
to_spec: 'media:a',
|
|
@@ -4624,6 +4670,7 @@ function test6486_Renderer_validateStrandStep_requiresBooleanIsSequence() {
|
|
|
4624
4670
|
let threw = false;
|
|
4625
4671
|
try {
|
|
4626
4672
|
rendererValidateStrandStep({
|
|
4673
|
+
token_id: nextFixtureStepToken(),
|
|
4627
4674
|
step_type: { Cap: {
|
|
4628
4675
|
cap_urn: 'cap:in="media:a";x;out="media:b"',
|
|
4629
4676
|
title: 't',
|
|
@@ -4645,13 +4692,13 @@ function test6487_Renderer_classifyStrandCapSteps_capFlags() {
|
|
|
4645
4692
|
// Strand: ForEach → cap1 → cap2 → cap3 → Collect. cap1 should have
|
|
4646
4693
|
// prevForEach=true; cap3 should have nextCollect=true; cap2 should
|
|
4647
4694
|
// have neither.
|
|
4648
|
-
const steps = [
|
|
4695
|
+
const steps = wireLinearStepInputs([
|
|
4649
4696
|
makeForEachStep('media:ext=pdf;list'),
|
|
4650
4697
|
makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=png;image"', 'a', 'media:ext=pdf', 'media:ext=png;image', false, false),
|
|
4651
4698
|
makeCapStep('cap:in="media:ext=png;image";b;out="media:ext=jpg"', 'b', 'media:ext=png;image', 'media:ext=jpg', false, false),
|
|
4652
4699
|
makeCapStep('cap:in="media:ext=jpg";c;out="media:ext=txt"', 'c', 'media:ext=jpg', 'media:ext=txt', false, false),
|
|
4653
4700
|
makeCollectStep('media:ext=txt'),
|
|
4654
|
-
];
|
|
4701
|
+
]);
|
|
4655
4702
|
const { capStepIndices, capFlags } = rendererClassifyStrandCapSteps(steps);
|
|
4656
4703
|
assertEqual(capStepIndices.length, 3, 'three cap steps');
|
|
4657
4704
|
assert(capFlags.get(1).prevForEach, 'cap1 has prevForEach');
|
|
@@ -4667,7 +4714,7 @@ function test6488_Renderer_classifyStrandCapSteps_nestedForks() {
|
|
|
4667
4714
|
// Nested strand: ForEach → cap1 → ForEach → cap2 → Collect → cap3 → Collect.
|
|
4668
4715
|
// cap1 has prevForEach (outer), cap2 has prevForEach (inner) and
|
|
4669
4716
|
// nextCollect (inner), cap3 has nextCollect (outer).
|
|
4670
|
-
const steps = [
|
|
4717
|
+
const steps = wireLinearStepInputs([
|
|
4671
4718
|
makeForEachStep('media:a;list'),
|
|
4672
4719
|
makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4673
4720
|
makeForEachStep('media:b;list'),
|
|
@@ -4675,7 +4722,7 @@ function test6488_Renderer_classifyStrandCapSteps_nestedForks() {
|
|
|
4675
4722
|
makeCollectStep('media:c'),
|
|
4676
4723
|
makeCapStep('cap:in="media:c";c;out="media:d"', 'c', 'media:c', 'media:d', false, false),
|
|
4677
4724
|
makeCollectStep('media:d'),
|
|
4678
|
-
];
|
|
4725
|
+
]);
|
|
4679
4726
|
const { capFlags } = rendererClassifyStrandCapSteps(steps);
|
|
4680
4727
|
assert(capFlags.get(1).prevForEach && !capFlags.get(1).nextCollect, 'cap1 outer entry');
|
|
4681
4728
|
assert(capFlags.get(3).prevForEach && capFlags.get(3).nextCollect, 'cap2 inner both');
|
|
@@ -4702,9 +4749,9 @@ function test6489_Renderer_buildStrandGraphData_singleCapPlain() {
|
|
|
4702
4749
|
const payload = withMediaDisplayNames({
|
|
4703
4750
|
source_media_urn: 'media:a',
|
|
4704
4751
|
target_media_urn: 'media:b',
|
|
4705
|
-
steps: [
|
|
4752
|
+
steps: wireLinearStepInputs([
|
|
4706
4753
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4707
|
-
],
|
|
4754
|
+
]),
|
|
4708
4755
|
}, {
|
|
4709
4756
|
'media:a': 'Source A',
|
|
4710
4757
|
'media:b': 'Target B',
|
|
@@ -4728,9 +4775,9 @@ function test6491_Renderer_buildStrandGraphData_sequenceShowsCardinality() {
|
|
|
4728
4775
|
const payload = withMediaDisplayNames({
|
|
4729
4776
|
source_media_urn: 'media:a;list',
|
|
4730
4777
|
target_media_urn: 'media:b',
|
|
4731
|
-
steps: [
|
|
4778
|
+
steps: wireLinearStepInputs([
|
|
4732
4779
|
makeCapStep('cap:in="media:a;list";x;out="media:b"', 'x', 'media:a;list', 'media:b', true, false),
|
|
4733
|
-
],
|
|
4780
|
+
]),
|
|
4734
4781
|
}, {
|
|
4735
4782
|
'media:a;list': 'Source A List',
|
|
4736
4783
|
'media:b': 'Target B',
|
|
@@ -4759,11 +4806,11 @@ function test6492_Renderer_buildStrandGraphData_foreachCollectSpan() {
|
|
|
4759
4806
|
const payload = withMediaDisplayNames({
|
|
4760
4807
|
source_media_urn: 'media:ext=pdf;list',
|
|
4761
4808
|
target_media_urn: 'media:txt;list',
|
|
4762
|
-
steps: [
|
|
4809
|
+
steps: wireLinearStepInputs([
|
|
4763
4810
|
makeForEachStep('media:ext=pdf;list'),
|
|
4764
4811
|
makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
|
|
4765
4812
|
makeCollectStep('media:ext=txt'),
|
|
4766
|
-
],
|
|
4813
|
+
]),
|
|
4767
4814
|
}, {
|
|
4768
4815
|
'media:ext=pdf;list': 'PDF List',
|
|
4769
4816
|
'media:ext=txt': 'Plain Text',
|
|
@@ -4802,10 +4849,10 @@ function test6493_Renderer_buildStrandGraphData_standaloneCollect() {
|
|
|
4802
4849
|
const payload = withMediaDisplayNames({
|
|
4803
4850
|
source_media_urn: 'media:a',
|
|
4804
4851
|
target_media_urn: 'media:b;list',
|
|
4805
|
-
steps: [
|
|
4852
|
+
steps: wireLinearStepInputs([
|
|
4806
4853
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4807
4854
|
makeCollectStep('media:b'),
|
|
4808
|
-
],
|
|
4855
|
+
]),
|
|
4809
4856
|
}, {
|
|
4810
4857
|
'media:a': 'Source A',
|
|
4811
4858
|
'media:b': 'Target B',
|
|
@@ -4831,11 +4878,11 @@ function test6494_Renderer_buildStrandGraphData_unclosedForEachBody() {
|
|
|
4831
4878
|
const payload = withMediaDisplayNames({
|
|
4832
4879
|
source_media_urn: 'media:a',
|
|
4833
4880
|
target_media_urn: 'media:c',
|
|
4834
|
-
steps: [
|
|
4881
|
+
steps: wireLinearStepInputs([
|
|
4835
4882
|
makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4836
4883
|
makeForEachStep('media:b'),
|
|
4837
4884
|
makeCapStep('cap:in="media:b";b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
|
|
4838
|
-
],
|
|
4885
|
+
]),
|
|
4839
4886
|
}, {
|
|
4840
4887
|
'media:a': 'Source A',
|
|
4841
4888
|
'media:b': 'Intermediate B',
|
|
@@ -4869,11 +4916,11 @@ function test6495_Renderer_buildStrandGraphData_nestedForEachThrows() {
|
|
|
4869
4916
|
const payload = withMediaDisplayNames({
|
|
4870
4917
|
source_media_urn: 'media:a;list',
|
|
4871
4918
|
target_media_urn: 'media:a',
|
|
4872
|
-
steps: [
|
|
4919
|
+
steps: wireLinearStepInputs([
|
|
4873
4920
|
makeForEachStep('media:a;list'),
|
|
4874
4921
|
makeForEachStep('media:a'),
|
|
4875
4922
|
makeCapStep('cap:in="media:a";x;out="media:a"', 'x', 'media:a', 'media:a', false, false),
|
|
4876
|
-
],
|
|
4923
|
+
]),
|
|
4877
4924
|
}, {
|
|
4878
4925
|
'media:a;list': 'Source A List',
|
|
4879
4926
|
'media:a': 'Source A',
|
|
@@ -4907,11 +4954,11 @@ function test6496_Renderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
|
|
|
4907
4954
|
const payload = withMediaDisplayNames({
|
|
4908
4955
|
source_media_urn: 'media:ext=pdf;list',
|
|
4909
4956
|
target_media_urn: 'media:txt;list',
|
|
4910
|
-
steps: [
|
|
4957
|
+
steps: wireLinearStepInputs([
|
|
4911
4958
|
makeForEachStep('media:ext=pdf;list'),
|
|
4912
4959
|
makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
|
|
4913
4960
|
makeCollectStep('media:ext=txt'),
|
|
4914
|
-
],
|
|
4961
|
+
]),
|
|
4915
4962
|
}, {
|
|
4916
4963
|
'media:ext=pdf;list': 'PDF List',
|
|
4917
4964
|
'media:ext=txt': 'Plain Text',
|
|
@@ -4957,11 +5004,11 @@ function test6497_Renderer_collapseStrand_unclosedForEachBodyCollapses() {
|
|
|
4957
5004
|
const payload = withMediaDisplayNames({
|
|
4958
5005
|
source_media_urn: 'media:a',
|
|
4959
5006
|
target_media_urn: 'media:c',
|
|
4960
|
-
steps: [
|
|
5007
|
+
steps: wireLinearStepInputs([
|
|
4961
5008
|
makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4962
5009
|
makeForEachStep('media:b'),
|
|
4963
5010
|
makeCapStep('cap:in="media:b";b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
|
|
4964
|
-
],
|
|
5011
|
+
]),
|
|
4965
5012
|
}, {
|
|
4966
5013
|
'media:a': 'Source A',
|
|
4967
5014
|
'media:b': 'Intermediate B',
|
|
@@ -5015,10 +5062,10 @@ function test6498_Renderer_collapseStrand_standaloneCollectCollapses() {
|
|
|
5015
5062
|
const payload = withMediaDisplayNames({
|
|
5016
5063
|
source_media_urn: 'media:a',
|
|
5017
5064
|
target_media_urn: 'media:b;list',
|
|
5018
|
-
steps: [
|
|
5065
|
+
steps: wireLinearStepInputs([
|
|
5019
5066
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
5020
5067
|
makeCollectStep('media:b'),
|
|
5021
|
-
],
|
|
5068
|
+
]),
|
|
5022
5069
|
}, {
|
|
5023
5070
|
'media:a': 'Source A',
|
|
5024
5071
|
'media:b': 'Target B',
|
|
@@ -5064,11 +5111,11 @@ function test6499_Renderer_collapseStrand_sequenceProducingCapBeforeForeach() {
|
|
|
5064
5111
|
const payload = withMediaDisplayNames({
|
|
5065
5112
|
source_media_urn: 'media:ext=pdf',
|
|
5066
5113
|
target_media_urn: 'media:decision',
|
|
5067
|
-
steps: [
|
|
5114
|
+
steps: wireLinearStepInputs([
|
|
5068
5115
|
makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
|
|
5069
5116
|
makeForEachStep('media:page'),
|
|
5070
5117
|
makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
5071
|
-
],
|
|
5118
|
+
]),
|
|
5072
5119
|
}, {
|
|
5073
5120
|
'media:ext=pdf': 'PDF',
|
|
5074
5121
|
'media:page': 'Page',
|
|
@@ -5120,9 +5167,9 @@ function test6500_Renderer_collapseStrand_plainCapMergesTrailingOutput() {
|
|
|
5120
5167
|
const payload = withMediaDisplayNames({
|
|
5121
5168
|
source_media_urn: 'media:a',
|
|
5122
5169
|
target_media_urn: 'media:b',
|
|
5123
|
-
steps: [
|
|
5170
|
+
steps: wireLinearStepInputs([
|
|
5124
5171
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
5125
|
-
],
|
|
5172
|
+
]),
|
|
5126
5173
|
}, {
|
|
5127
5174
|
'media:a': 'Source A',
|
|
5128
5175
|
'media:b': 'Target B',
|
|
@@ -5153,9 +5200,9 @@ function test6501_Renderer_collapseStrand_plainCapDistinctTargetNoMerge() {
|
|
|
5153
5200
|
const payload = withMediaDisplayNames({
|
|
5154
5201
|
source_media_urn: 'media:a',
|
|
5155
5202
|
target_media_urn: 'media:b;list',
|
|
5156
|
-
steps: [
|
|
5203
|
+
steps: wireLinearStepInputs([
|
|
5157
5204
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
5158
|
-
],
|
|
5205
|
+
]),
|
|
5159
5206
|
}, {
|
|
5160
5207
|
'media:a': 'Source A',
|
|
5161
5208
|
'media:b': 'Target B',
|
|
@@ -5184,24 +5231,175 @@ function test6502_Renderer_validateStrandPayload_missingSourceMediaUrn() {
|
|
|
5184
5231
|
assert(threw, 'missing source_media_urn must throw');
|
|
5185
5232
|
}
|
|
5186
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
|
+
|
|
5187
5277
|
// ---------------- run builder ----------------
|
|
5188
5278
|
|
|
5189
5279
|
function test6503_Renderer_validateBodyOutcome_rejectsNegativeIndex() {
|
|
5190
5280
|
let threw = false;
|
|
5191
5281
|
try {
|
|
5192
|
-
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');
|
|
5193
5283
|
} catch (e) {
|
|
5194
5284
|
threw = true;
|
|
5195
5285
|
}
|
|
5196
5286
|
assert(threw, 'negative body_index must throw');
|
|
5197
5287
|
}
|
|
5198
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
|
+
|
|
5199
5397
|
// TEST6504: Renderer build run graph data pages successes and failures
|
|
5200
5398
|
function test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
|
|
5201
5399
|
// 6 successes, 4 failures. visible=3+2, total=10. Body has 2
|
|
5202
5400
|
// caps (a, b). Each body replica is a chain of:
|
|
5203
5401
|
// entry node + body_step_0 (cap a) + body_step_1 (cap b)
|
|
5204
|
-
// = 3 nodes per body. Failed bodies truncate at
|
|
5402
|
+
// = 3 nodes per body. Failed bodies truncate at failed_token_id
|
|
5205
5403
|
// (cap b, idx=1) so `traceEnd=2` — same 3-node chain.
|
|
5206
5404
|
//
|
|
5207
5405
|
// Total replica nodes: (3 success × 3) + (2 failure × 3) = 15.
|
|
@@ -5211,26 +5409,28 @@ function test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
|
|
|
5211
5409
|
const strand = {
|
|
5212
5410
|
source_media_urn: 'media:ext=pdf;list',
|
|
5213
5411
|
target_media_urn: 'media:ext=txt',
|
|
5214
|
-
steps: [
|
|
5412
|
+
steps: wireLinearStepInputs([
|
|
5215
5413
|
makeForEachStep('media:ext=pdf;list'),
|
|
5216
5414
|
makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=png;image"', 'a', 'media:ext=pdf', 'media:ext=png;image', false, false),
|
|
5217
5415
|
makeCapStep('cap:in="media:ext=png;image";b;out="media:ext=txt"', 'b', 'media:ext=png;image', 'media:ext=txt', false, false),
|
|
5218
5416
|
makeCollectStep('media:ext=txt'),
|
|
5219
|
-
],
|
|
5417
|
+
]),
|
|
5220
5418
|
};
|
|
5221
5419
|
const outcomes = [];
|
|
5222
5420
|
for (let i = 0; i < 6; i++) {
|
|
5223
|
-
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 });
|
|
5224
5422
|
}
|
|
5225
5423
|
for (let i = 6; i < 10; i++) {
|
|
5226
5424
|
outcomes.push({
|
|
5227
5425
|
body_index: i,
|
|
5426
|
+
foreach_token_id: strand.steps[0].token_id,
|
|
5228
5427
|
success: false,
|
|
5229
5428
|
cap_urns: [],
|
|
5230
5429
|
saved_paths: [],
|
|
5231
5430
|
total_bytes: 0,
|
|
5232
5431
|
duration_ms: 0,
|
|
5233
|
-
|
|
5432
|
+
failed_token_id: strand.steps[2].token_id,
|
|
5433
|
+
failed_arg_urn: null,
|
|
5234
5434
|
error: 'oom',
|
|
5235
5435
|
});
|
|
5236
5436
|
}
|
|
@@ -5267,22 +5467,21 @@ function test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
|
|
|
5267
5467
|
assertEqual(failureShowMore.data.hiddenCount, 2, 'failure hidden count = 2');
|
|
5268
5468
|
}
|
|
5269
5469
|
|
|
5270
|
-
// TEST6505: Renderer
|
|
5271
|
-
function
|
|
5272
|
-
// A failure without
|
|
5273
|
-
//
|
|
5274
|
-
// 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.
|
|
5275
5474
|
//
|
|
5276
5475
|
// Strand [ForEach, Cap, Collect] → body has 1 cap. Each body
|
|
5277
5476
|
// replica emits 1 entry node + 1 body cap node = 2 nodes.
|
|
5278
5477
|
const strand = {
|
|
5279
5478
|
source_media_urn: 'media:ext=pdf;list',
|
|
5280
5479
|
target_media_urn: 'media:ext=txt',
|
|
5281
|
-
steps: [
|
|
5480
|
+
steps: wireLinearStepInputs([
|
|
5282
5481
|
makeForEachStep('media:ext=pdf;list'),
|
|
5283
5482
|
makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=txt"', 'a', 'media:ext=pdf', 'media:ext=txt', false, false),
|
|
5284
5483
|
makeCollectStep('media:ext=txt'),
|
|
5285
|
-
],
|
|
5484
|
+
]),
|
|
5286
5485
|
};
|
|
5287
5486
|
const payload = {
|
|
5288
5487
|
resolved_strand: strand,
|
|
@@ -5292,7 +5491,7 @@ function test6505_Renderer_buildRunGraphData_failureWithoutFailedCapRendersFullT
|
|
|
5292
5491
|
'media:ext=txt': 'Text',
|
|
5293
5492
|
},
|
|
5294
5493
|
body_outcomes: [
|
|
5295
|
-
{ 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' },
|
|
5296
5495
|
],
|
|
5297
5496
|
visible_success_count: 0,
|
|
5298
5497
|
visible_failure_count: 5,
|
|
@@ -5303,19 +5502,15 @@ function test6505_Renderer_buildRunGraphData_failureWithoutFailedCapRendersFullT
|
|
|
5303
5502
|
for (const n of built.replicaNodes) {
|
|
5304
5503
|
if (n.classes === 'body-failure') failureNodes++;
|
|
5305
5504
|
}
|
|
5306
|
-
assertEqual(failureNodes,
|
|
5505
|
+
assertEqual(failureNodes, 1, 'only the body entry is shown for an unattributed failure');
|
|
5307
5506
|
}
|
|
5308
5507
|
|
|
5309
|
-
// TEST6506: Renderer
|
|
5310
|
-
function
|
|
5311
|
-
// The renderer matches failed_cap against step cap URNs via
|
|
5312
|
-
// CapUrn.isEquivalent, NOT string equality. Feed a payload where
|
|
5313
|
-
// failed_cap and the step's cap_urn differ only in tag order — they
|
|
5314
|
-
// 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() {
|
|
5315
5510
|
const strand = {
|
|
5316
5511
|
source_media_urn: 'media:a',
|
|
5317
5512
|
target_media_urn: 'media:c',
|
|
5318
|
-
steps: [
|
|
5513
|
+
steps: wireLinearStepInputs([
|
|
5319
5514
|
makeForEachStep('media:a;list'),
|
|
5320
5515
|
// Canonical form places tags alphabetically: op after in/out.
|
|
5321
5516
|
makeCapStep(
|
|
@@ -5327,11 +5522,8 @@ function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap(
|
|
|
5327
5522
|
'y', 'media:b', 'media:c', false, false
|
|
5328
5523
|
),
|
|
5329
5524
|
makeCollectStep('media:c'),
|
|
5330
|
-
],
|
|
5525
|
+
]),
|
|
5331
5526
|
};
|
|
5332
|
-
// The failed_cap URN is semantically the same as step 1 (cap y). If
|
|
5333
|
-
// CapUrn.fromString canonicalizes (it should), any equivalent form
|
|
5334
|
-
// will match. Feed a fully-specified form that is equivalent.
|
|
5335
5527
|
const payload = {
|
|
5336
5528
|
resolved_strand: strand,
|
|
5337
5529
|
media_display_names: {
|
|
@@ -5343,12 +5535,14 @@ function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap(
|
|
|
5343
5535
|
body_outcomes: [
|
|
5344
5536
|
{
|
|
5345
5537
|
body_index: 0,
|
|
5538
|
+
foreach_token_id: strand.steps[0].token_id,
|
|
5346
5539
|
success: false,
|
|
5347
5540
|
cap_urns: [],
|
|
5348
5541
|
saved_paths: [],
|
|
5349
5542
|
total_bytes: 0,
|
|
5350
5543
|
duration_ms: 0,
|
|
5351
|
-
|
|
5544
|
+
failed_token_id: strand.steps[2].token_id,
|
|
5545
|
+
failed_arg_urn: null,
|
|
5352
5546
|
error: 'fail',
|
|
5353
5547
|
},
|
|
5354
5548
|
],
|
|
@@ -5363,7 +5557,19 @@ function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap(
|
|
|
5363
5557
|
}
|
|
5364
5558
|
// 1 entry + 2 body step nodes (cap x and cap y, truncated
|
|
5365
5559
|
// at cap y) = 3 failure replica nodes.
|
|
5366
|
-
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');
|
|
5367
5573
|
}
|
|
5368
5574
|
|
|
5369
5575
|
// TEST6507: Renderer build run graph data backbone has no foreach node
|
|
@@ -5382,11 +5588,11 @@ function test6507_Renderer_buildRunGraphData_backboneHasNoForeachNode() {
|
|
|
5382
5588
|
const strand = {
|
|
5383
5589
|
source_media_urn: 'media:ext=pdf',
|
|
5384
5590
|
target_media_urn: 'media:decision',
|
|
5385
|
-
steps: [
|
|
5591
|
+
steps: wireLinearStepInputs([
|
|
5386
5592
|
makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
|
|
5387
5593
|
makeForEachStep('media:page'),
|
|
5388
5594
|
makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
5389
|
-
],
|
|
5595
|
+
]),
|
|
5390
5596
|
};
|
|
5391
5597
|
const payload = {
|
|
5392
5598
|
resolved_strand: strand,
|
|
@@ -5431,13 +5637,12 @@ function test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
|
|
|
5431
5637
|
const strand = {
|
|
5432
5638
|
source_media_urn: 'media:ext=pdf',
|
|
5433
5639
|
target_media_urn: 'media:decision',
|
|
5434
|
-
steps: [
|
|
5640
|
+
steps: wireLinearStepInputs([
|
|
5435
5641
|
makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
|
|
5436
5642
|
makeForEachStep('media:page'),
|
|
5437
5643
|
makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
5438
|
-
],
|
|
5644
|
+
]),
|
|
5439
5645
|
};
|
|
5440
|
-
const failedCapUrn = 'cap:in="media:page";decide;out="media:decision"';
|
|
5441
5646
|
const payload = {
|
|
5442
5647
|
resolved_strand: strand,
|
|
5443
5648
|
media_display_names: {
|
|
@@ -5446,8 +5651,8 @@ function test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
|
|
|
5446
5651
|
'media:decision': 'Decision',
|
|
5447
5652
|
},
|
|
5448
5653
|
body_outcomes: [
|
|
5449
|
-
{ body_index: 0, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0,
|
|
5450
|
-
{ 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' },
|
|
5451
5656
|
],
|
|
5452
5657
|
visible_success_count: 3,
|
|
5453
5658
|
visible_failure_count: 3,
|
|
@@ -5502,11 +5707,11 @@ function test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
|
|
|
5502
5707
|
const strand = {
|
|
5503
5708
|
source_media_urn: 'media:ext=pdf',
|
|
5504
5709
|
target_media_urn: 'media:decision',
|
|
5505
|
-
steps: [
|
|
5710
|
+
steps: wireLinearStepInputs([
|
|
5506
5711
|
makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
|
|
5507
5712
|
makeForEachStep('media:page'),
|
|
5508
5713
|
makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
5509
|
-
],
|
|
5714
|
+
]),
|
|
5510
5715
|
};
|
|
5511
5716
|
const payload = {
|
|
5512
5717
|
resolved_strand: strand,
|
|
@@ -5516,7 +5721,7 @@ function test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
|
|
|
5516
5721
|
'media:decision': 'Decision',
|
|
5517
5722
|
},
|
|
5518
5723
|
body_outcomes: [
|
|
5519
|
-
{ 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 },
|
|
5520
5725
|
],
|
|
5521
5726
|
visible_success_count: 3,
|
|
5522
5727
|
visible_failure_count: 3,
|
|
@@ -5562,11 +5767,11 @@ function test6510_Renderer_buildRunGraphData_closedForeachSuccessMergesAtCollect
|
|
|
5562
5767
|
const strand = {
|
|
5563
5768
|
source_media_urn: 'media:ext=pdf;list',
|
|
5564
5769
|
target_media_urn: 'media:txt;list',
|
|
5565
|
-
steps: [
|
|
5770
|
+
steps: wireLinearStepInputs([
|
|
5566
5771
|
makeForEachStep('media:ext=pdf;list'),
|
|
5567
5772
|
makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
|
|
5568
5773
|
makeCollectStep('media:ext=txt'),
|
|
5569
|
-
],
|
|
5774
|
+
]),
|
|
5570
5775
|
};
|
|
5571
5776
|
const payload = {
|
|
5572
5777
|
resolved_strand: strand,
|
|
@@ -5577,7 +5782,7 @@ function test6510_Renderer_buildRunGraphData_closedForeachSuccessMergesAtCollect
|
|
|
5577
5782
|
'media:txt;list': 'Text List',
|
|
5578
5783
|
},
|
|
5579
5784
|
body_outcomes: [
|
|
5580
|
-
{ 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 },
|
|
5581
5786
|
],
|
|
5582
5787
|
visible_success_count: 3,
|
|
5583
5788
|
visible_failure_count: 3,
|
|
@@ -6362,6 +6567,139 @@ function test1848_capVersionNonZeroOnWire() {
|
|
|
6362
6567
|
assertEqual(restored.version, 42, 'Restored version must equal 42');
|
|
6363
6568
|
}
|
|
6364
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
|
+
|
|
6365
6703
|
// ===========================================================================
|
|
6366
6704
|
// Fabric alias tests (shared numbers 1880-1882, 1887)
|
|
6367
6705
|
//
|
|
@@ -6886,7 +7224,7 @@ async function runTests() {
|
|
|
6886
7224
|
await runTest('TEST1875: scan_all_reaches_both_dev_and_registry_slugs', test1875_scanAllReachesBothDevAndRegistrySlugs);
|
|
6887
7225
|
await runTest('TEST1876: other_channel_subtree_is_skipped', test1876_otherChannelSubtreeIsSkipped);
|
|
6888
7226
|
await runTest('TEST1877: registry_cartridge_under_wrong_slug_is_bad_install', test1877_registryCartridgeUnderWrongSlugIsBadInstall);
|
|
6889
|
-
await runTest('TEST1878:
|
|
7227
|
+
await runTest('TEST1878: bundled_cartridge_without_baked_hash_is_rejected', test1878_bundledCartridgeWithoutBakedHashIsRejected);
|
|
6890
7228
|
|
|
6891
7229
|
// media_urn.rs: TEST1312-TEST1315, TEST1298-TEST1302 (MediaUrn predicates)
|
|
6892
7230
|
console.log('\n--- media_urn.rs (predicates) ---');
|
|
@@ -7077,12 +7415,14 @@ async function runTests() {
|
|
|
7077
7415
|
runTest('RENDERER: collapseStrand_plainCapMergesOutput', test6500_Renderer_collapseStrand_plainCapMergesTrailingOutput);
|
|
7078
7416
|
runTest('RENDERER: collapseStrand_plainCapDistinctTarget', test6501_Renderer_collapseStrand_plainCapDistinctTargetNoMerge);
|
|
7079
7417
|
runTest('RENDERER: validateStrand_missingSourceMediaUrn', test6502_Renderer_validateStrandPayload_missingSourceMediaUrn);
|
|
7418
|
+
runTest('RENDERER: validateStrand_requiresCapInputs', test7120_Renderer_validateStrandPayload_requiresAuthoritativeInputs);
|
|
7080
7419
|
|
|
7081
7420
|
console.log('\n--- cap-fab-renderer run builder ---');
|
|
7082
7421
|
runTest('RENDERER: validateBodyOutcome_negativeIndex', test6503_Renderer_validateBodyOutcome_rejectsNegativeIndex);
|
|
7422
|
+
runTest('RENDERER: validateBodyOutcome_failureCoordinate', test7121_Renderer_validateBodyOutcome_requiresExplicitFailureCoordinate);
|
|
7083
7423
|
runTest('RENDERER: buildRun_pagesSuccessesAndFailures', test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures);
|
|
7084
|
-
runTest('RENDERER:
|
|
7085
|
-
runTest('RENDERER:
|
|
7424
|
+
runTest('RENDERER: buildRun_unattributedFailureAtEntry', test6505_Renderer_buildRunGraphData_unattributedFailureStopsAtEntry);
|
|
7425
|
+
runTest('RENDERER: buildRun_resolvesFailedStepToken', test6506_Renderer_buildRunGraphData_resolvesExactFailedStepToken);
|
|
7086
7426
|
runTest('RENDERER: buildRun_backboneHasNoForeachNode', test6507_Renderer_buildRunGraphData_backboneHasNoForeachNode);
|
|
7087
7427
|
runTest('RENDERER: buildRun_allFailedDropsPlaceholder', test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder);
|
|
7088
7428
|
runTest('RENDERER: buildRun_unclosedForeachNoMerge', test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge);
|
|
@@ -7141,6 +7481,13 @@ async function runTests() {
|
|
|
7141
7481
|
runTest('TEST1847: cap_version_zero_omitted_on_wire', test6737_capVersionZeroOmittedOnWire);
|
|
7142
7482
|
runTest('TEST1848: cap_version_nonzero_on_wire', test1848_capVersionNonZeroOnWire);
|
|
7143
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
|
+
|
|
7144
7491
|
runTest('TEST1880: alias_name_normalization_rules', test1880_aliasNameNormalizationRules);
|
|
7145
7492
|
runTest('TEST1881: token_urn_vs_alias_detection', test1881_tokenUrnVsAliasDetection);
|
|
7146
7493
|
runTest('TEST1882: classify_alias_target_by_prefix', test1882_classifyAliasTargetByPrefix);
|