capdag 0.106.243 → 0.119.263
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-graph-renderer.js +756 -170
- package/capdag.test.js +574 -57
- package/machine-parser.js +126 -64
- package/package.json +1 -1
package/capdag.test.js
CHANGED
|
@@ -3755,6 +3755,7 @@ const {
|
|
|
3755
3755
|
canonicalMediaUrn: rendererCanonicalMediaUrn,
|
|
3756
3756
|
mediaNodeLabel: rendererMediaNodeLabel,
|
|
3757
3757
|
buildStrandGraphData: rendererBuildStrandGraphData,
|
|
3758
|
+
collapseStrandShapeTransitions: rendererCollapseStrandShapeTransitions,
|
|
3758
3759
|
buildRunGraphData: rendererBuildRunGraphData,
|
|
3759
3760
|
buildMachineGraphData: rendererBuildMachineGraphData,
|
|
3760
3761
|
classifyStrandCapSteps: rendererClassifyStrandCapSteps,
|
|
@@ -4030,86 +4031,441 @@ function testRenderer_classifyStrandCapSteps_nestedForks() {
|
|
|
4030
4031
|
assert(!capFlags.get(5).prevForEach && capFlags.get(5).nextCollect, 'cap3 outer exit');
|
|
4031
4032
|
}
|
|
4032
4033
|
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
//
|
|
4040
|
-
//
|
|
4041
|
-
//
|
|
4034
|
+
// Helper: find an edge with the given source/target ids.
|
|
4035
|
+
function findEdge(edges, source, target) {
|
|
4036
|
+
return edges.find(e => e.source === source && e.target === target);
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
function testRenderer_buildStrandGraphData_singleCapPlain() {
|
|
4040
|
+
// Minimal strand with one plain 1→1 cap. Plan builder produces:
|
|
4041
|
+
// input_slot → step_0 (cap) → output
|
|
4042
|
+
// (two edges, three nodes). No cardinality marker in the cap label
|
|
4043
|
+
// because input_is_sequence == output_is_sequence == false.
|
|
4044
|
+
const payload = {
|
|
4045
|
+
source_spec: 'media:a',
|
|
4046
|
+
target_spec: 'media:b',
|
|
4047
|
+
steps: [
|
|
4048
|
+
makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4049
|
+
],
|
|
4050
|
+
};
|
|
4051
|
+
const built = rendererBuildStrandGraphData(payload);
|
|
4052
|
+
const nodeIds = built.nodes.map(n => n.id).sort();
|
|
4053
|
+
assertEqual(JSON.stringify(nodeIds), JSON.stringify(['input_slot', 'output', 'step_0']),
|
|
4054
|
+
'nodes are input_slot + step_0 + output (positional ids)');
|
|
4055
|
+
assertEqual(built.edges.length, 2, 'two edges: input_slot→step_0 and step_0→output');
|
|
4056
|
+
const capEdge = findEdge(built.edges, 'input_slot', 'step_0');
|
|
4057
|
+
assert(capEdge !== undefined, 'cap edge from input_slot to step_0 exists');
|
|
4058
|
+
assertEqual(capEdge.label, 'x', 'plain cap edge label is the cap title with no cardinality marker');
|
|
4059
|
+
const outEdge = findEdge(built.edges, 'step_0', 'output');
|
|
4060
|
+
assert(outEdge !== undefined, 'output edge from step_0 to output exists');
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
function testRenderer_buildStrandGraphData_sequenceShowsCardinality() {
|
|
4064
|
+
// A cap with input_is_sequence=true MUST emit "(n→1)" on its edge
|
|
4065
|
+
// label.
|
|
4066
|
+
const payload = {
|
|
4067
|
+
source_spec: 'media:a;list',
|
|
4068
|
+
target_spec: 'media:b',
|
|
4069
|
+
steps: [
|
|
4070
|
+
makeCapStep('cap:in="media:a;list";op=x;out="media:b"', 'x', 'media:a;list', 'media:b', true, false),
|
|
4071
|
+
],
|
|
4072
|
+
};
|
|
4073
|
+
const built = rendererBuildStrandGraphData(payload);
|
|
4074
|
+
const capEdge = findEdge(built.edges, 'input_slot', 'step_0');
|
|
4075
|
+
assert(capEdge !== undefined, 'cap edge exists');
|
|
4076
|
+
assert(capEdge.label.includes('(n\u21921)'),
|
|
4077
|
+
`cap edge label must include (n\u21921) marker; got: ${capEdge.label}`);
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
function testRenderer_buildStrandGraphData_foreachCollectSpan() {
|
|
4081
|
+
// Strand: [ForEach, Cap, Collect]. Plan builder produces:
|
|
4082
|
+
// input_slot (source) →direct→ step_1 (cap) — cap emits its own
|
|
4083
|
+
// direct edge from prev
|
|
4084
|
+
// input_slot →direct→ step_0 (foreach) — created when Collect
|
|
4085
|
+
// step_0 →iteration→ step_1 — iteration edge
|
|
4086
|
+
// step_1 →collection→ step_2 (collect) — collection edge
|
|
4087
|
+
// step_2 →direct→ output — output connector
|
|
4088
|
+
//
|
|
4089
|
+
// (six nodes: input_slot, step_0, step_1, step_2, output; five
|
|
4090
|
+
// edges.) ForEach and Collect are REAL nodes in the graph, not
|
|
4091
|
+
// labels on cap edges — they're distinct processing units in the
|
|
4092
|
+
// plan. This mirrors capdag's plan_builder.rs exactly.
|
|
4042
4093
|
const payload = {
|
|
4043
4094
|
source_spec: 'media:pdf;list',
|
|
4044
|
-
target_spec: 'media:txt',
|
|
4095
|
+
target_spec: 'media:txt;list',
|
|
4045
4096
|
steps: [
|
|
4046
4097
|
makeForEachStep('media:pdf;list'),
|
|
4047
|
-
makeCapStep('cap:in="media:pdf";op=extract;out="media:txt"', 'extract
|
|
4098
|
+
makeCapStep('cap:in="media:pdf";op=extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
|
|
4099
|
+
makeCollectStep('media:txt'),
|
|
4048
4100
|
],
|
|
4049
4101
|
};
|
|
4050
4102
|
const built = rendererBuildStrandGraphData(payload);
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4103
|
+
const nodeIds = built.nodes.map(n => n.id).sort();
|
|
4104
|
+
assertEqual(JSON.stringify(nodeIds),
|
|
4105
|
+
JSON.stringify(['input_slot', 'output', 'step_0', 'step_1', 'step_2']),
|
|
4106
|
+
'positional nodes for source, foreach, cap, collect, output');
|
|
4107
|
+
|
|
4108
|
+
// The five edges the plan builder would produce:
|
|
4109
|
+
assert(findEdge(built.edges, 'input_slot', 'step_1') !== undefined,
|
|
4110
|
+
'cap direct edge input_slot→step_1 (prev wasn\'t advanced by ForEach)');
|
|
4111
|
+
assert(findEdge(built.edges, 'input_slot', 'step_0') !== undefined,
|
|
4112
|
+
'foreach input edge input_slot→step_0');
|
|
4113
|
+
assert(findEdge(built.edges, 'step_0', 'step_1') !== undefined,
|
|
4114
|
+
'iteration edge step_0→step_1 (body entry)');
|
|
4115
|
+
assert(findEdge(built.edges, 'step_1', 'step_2') !== undefined,
|
|
4116
|
+
'collection edge step_1→step_2 (body exit → collect)');
|
|
4117
|
+
assert(findEdge(built.edges, 'step_2', 'output') !== undefined,
|
|
4118
|
+
'output edge step_2→output');
|
|
4119
|
+
|
|
4120
|
+
// ForEach and Collect nodes carry their canonical labels.
|
|
4121
|
+
const foreachNode = built.nodes.find(n => n.id === 'step_0');
|
|
4122
|
+
assertEqual(foreachNode.label, 'for each', 'ForEach node labeled "for each"');
|
|
4123
|
+
const collectNode = built.nodes.find(n => n.id === 'step_2');
|
|
4124
|
+
assertEqual(collectNode.label, 'collect', 'Collect node labeled "collect"');
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
function testRenderer_buildStrandGraphData_standaloneCollect() {
|
|
4128
|
+
// Strand with a standalone Collect (no enclosing ForEach). Plan
|
|
4129
|
+
// builder creates a Collect node consuming prev directly — plain
|
|
4130
|
+
// direct edge, no iteration/collection semantics.
|
|
4066
4131
|
const payload = {
|
|
4067
4132
|
source_spec: 'media:a',
|
|
4068
|
-
target_spec: 'media:
|
|
4133
|
+
target_spec: 'media:b;list',
|
|
4069
4134
|
steps: [
|
|
4070
|
-
makeCapStep('cap:in="media:a";op=x;out="media:
|
|
4071
|
-
makeCollectStep('media:
|
|
4135
|
+
makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4136
|
+
makeCollectStep('media:b'),
|
|
4072
4137
|
],
|
|
4073
4138
|
};
|
|
4074
4139
|
const built = rendererBuildStrandGraphData(payload);
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
//
|
|
4087
|
-
//
|
|
4140
|
+
assert(findEdge(built.edges, 'input_slot', 'step_0') !== undefined,
|
|
4141
|
+
'cap edge input_slot → step_0');
|
|
4142
|
+
assert(findEdge(built.edges, 'step_0', 'step_1') !== undefined,
|
|
4143
|
+
'standalone collect edge step_0 → step_1 (Collect node)');
|
|
4144
|
+
assert(findEdge(built.edges, 'step_1', 'output') !== undefined,
|
|
4145
|
+
'output edge step_1 → output');
|
|
4146
|
+
const collectNode = built.nodes.find(n => n.id === 'step_1');
|
|
4147
|
+
assertEqual(collectNode.label, 'collect', 'Collect node labeled "collect"');
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
function testRenderer_buildStrandGraphData_unclosedForEachBody() {
|
|
4151
|
+
// Strand: [Cap_a, ForEach, Cap_b] with no closing Collect. The plan
|
|
4152
|
+
// builder's "unclosed ForEach" branch creates a ForEach node
|
|
4153
|
+
// connecting Cap_a to Cap_b via iteration, with prev becoming the
|
|
4154
|
+
// body exit (Cap_b).
|
|
4088
4155
|
const payload = {
|
|
4089
4156
|
source_spec: 'media:a',
|
|
4090
|
-
target_spec: 'media:
|
|
4157
|
+
target_spec: 'media:c',
|
|
4158
|
+
steps: [
|
|
4159
|
+
makeCapStep('cap:in="media:a";op=a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4160
|
+
makeForEachStep('media:b'),
|
|
4161
|
+
makeCapStep('cap:in="media:b";op=b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
|
|
4162
|
+
],
|
|
4163
|
+
};
|
|
4164
|
+
const built = rendererBuildStrandGraphData(payload);
|
|
4165
|
+
// Cap_a connects from input_slot.
|
|
4166
|
+
assert(findEdge(built.edges, 'input_slot', 'step_0') !== undefined,
|
|
4167
|
+
'cap_a edge input_slot → step_0');
|
|
4168
|
+
// Cap_b still connects directly from step_0 (the ForEach didn't
|
|
4169
|
+
// advance prev). This mirrors plan_builder.
|
|
4170
|
+
assert(findEdge(built.edges, 'step_0', 'step_2') !== undefined,
|
|
4171
|
+
'cap_b direct edge step_0 → step_2');
|
|
4172
|
+
// ForEach node at step_1 with direct edge from step_0 and iteration
|
|
4173
|
+
// edge to step_2.
|
|
4174
|
+
assert(findEdge(built.edges, 'step_0', 'step_1') !== undefined,
|
|
4175
|
+
'foreach input edge step_0 → step_1');
|
|
4176
|
+
assert(findEdge(built.edges, 'step_1', 'step_2') !== undefined,
|
|
4177
|
+
'iteration edge step_1 → step_2 (body entry)');
|
|
4178
|
+
// Output connects from step_2 (body exit).
|
|
4179
|
+
assert(findEdge(built.edges, 'step_2', 'output') !== undefined,
|
|
4180
|
+
'output edge step_2 → output');
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
function testRenderer_buildStrandGraphData_nestedForEachThrows() {
|
|
4184
|
+
// Nested ForEach without an intervening body cap in the outer
|
|
4185
|
+
// ForEach is an illegal nesting per plan_builder. The renderer
|
|
4186
|
+
// must throw the same error to surface the issue rather than
|
|
4187
|
+
// render a malformed graph.
|
|
4188
|
+
const payload = {
|
|
4189
|
+
source_spec: 'media:a;list;list',
|
|
4190
|
+
target_spec: 'media:a',
|
|
4191
|
+
steps: [
|
|
4192
|
+
makeForEachStep('media:a;list;list'),
|
|
4193
|
+
makeForEachStep('media:a;list'),
|
|
4194
|
+
makeCapStep('cap:in="media:a";op=x;out="media:a"', 'x', 'media:a', 'media:a', false, false),
|
|
4195
|
+
],
|
|
4196
|
+
};
|
|
4197
|
+
let threw = false;
|
|
4198
|
+
try {
|
|
4199
|
+
rendererBuildStrandGraphData(payload);
|
|
4200
|
+
} catch (e) {
|
|
4201
|
+
threw = true;
|
|
4202
|
+
assert(e.message.includes('nested ForEach'),
|
|
4203
|
+
'error must name the nested-ForEach violation');
|
|
4204
|
+
}
|
|
4205
|
+
assert(threw, 'nested ForEach without outer body cap must throw');
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
function testRenderer_collapseStrand_singleCapBodyShowsCapTitleWithIterCollectMarker() {
|
|
4209
|
+
// User spec: ForEach/Collect are NOT rendered as nodes. The
|
|
4210
|
+
// transition is labeled with the enclosed cap's title + a
|
|
4211
|
+
// cardinality marker. For a single-cap body the marker is n→n
|
|
4212
|
+
// (iterate + collect combined) because the same cap is both
|
|
4213
|
+
// the body entry and the body exit.
|
|
4214
|
+
//
|
|
4215
|
+
// Strand [ForEach, Cap(extract), Collect], source=pdf;list,
|
|
4216
|
+
// target=txt;list — target is NOT equivalent to cap's to_spec
|
|
4217
|
+
// media:txt so the output node is retained.
|
|
4218
|
+
//
|
|
4219
|
+
// Expected render shape: 3 nodes (input_slot, step_1, output),
|
|
4220
|
+
// with the entry edge labeled "extract (n→n)" and a plain
|
|
4221
|
+
// unlabeled connector to the output.
|
|
4222
|
+
const payload = {
|
|
4223
|
+
source_spec: 'media:pdf;list',
|
|
4224
|
+
target_spec: 'media:txt;list',
|
|
4225
|
+
steps: [
|
|
4226
|
+
makeForEachStep('media:pdf;list'),
|
|
4227
|
+
makeCapStep('cap:in="media:pdf";op=extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
|
|
4228
|
+
makeCollectStep('media:txt'),
|
|
4229
|
+
],
|
|
4230
|
+
};
|
|
4231
|
+
const built = rendererBuildStrandGraphData(payload);
|
|
4232
|
+
const collapsed = rendererCollapseStrandShapeTransitions(built);
|
|
4233
|
+
|
|
4234
|
+
const nodeIds = collapsed.nodes.map(n => n.id).sort();
|
|
4235
|
+
assertEqual(JSON.stringify(nodeIds),
|
|
4236
|
+
JSON.stringify(['input_slot', 'output', 'step_1']),
|
|
4237
|
+
'collapse removes the ForEach and Collect nodes; the remaining nodes are source + cap + target');
|
|
4238
|
+
|
|
4239
|
+
// Exactly one edge from input_slot → step_1, carrying the cap
|
|
4240
|
+
// title + iterate+collect cardinality marker.
|
|
4241
|
+
const entryEdges = collapsed.edges.filter(e => e.source === 'input_slot' && e.target === 'step_1');
|
|
4242
|
+
assertEqual(entryEdges.length, 1,
|
|
4243
|
+
'phantom duplicate cap edge must be gone — exactly one edge from source to cap');
|
|
4244
|
+
assertEqual(entryEdges[0].label, 'extract (n\u2192n)',
|
|
4245
|
+
'single-cap-body edge is labeled "<cap_title> (n→n)"');
|
|
4246
|
+
|
|
4247
|
+
// The exit side is a plain unlabeled connector — the cap title
|
|
4248
|
+
// is already shown on the entry edge.
|
|
4249
|
+
const exitEdges = collapsed.edges.filter(e => e.source === 'step_1' && e.target === 'output');
|
|
4250
|
+
assertEqual(exitEdges.length, 1,
|
|
4251
|
+
'there is exactly one exit edge step_1 → output');
|
|
4252
|
+
assertEqual(exitEdges[0].label, '',
|
|
4253
|
+
'exit connector for a single-cap body is unlabeled (cap title already shown on entry edge)');
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
function testRenderer_collapseStrand_unclosedForEachBodyCollapses() {
|
|
4257
|
+
// [Cap_a, ForEach, Cap_b] with no Collect, source=media:a,
|
|
4258
|
+
// target=media:c. Cap_b's to_spec is media:c which is
|
|
4259
|
+
// equivalent to target_spec, so the output node is merged
|
|
4260
|
+
// into step_2.
|
|
4261
|
+
//
|
|
4262
|
+
// Raw topology:
|
|
4263
|
+
// input_slot → step_0 (cap_a) — normal, not in any body
|
|
4264
|
+
// step_0 → step_2 (cap_b, foreachEntry=true — phantom under
|
|
4265
|
+
// plan builder, but the foreach body entry
|
|
4266
|
+
// under the render model)
|
|
4267
|
+
// step_0 → step_1 (foreach direct)
|
|
4268
|
+
// step_1 → step_2 (iteration)
|
|
4269
|
+
// step_2 → output (trailing connector, empty label)
|
|
4270
|
+
//
|
|
4271
|
+
// Collapse:
|
|
4272
|
+
// - step_1 (foreach) removed with its iteration edges.
|
|
4273
|
+
// - step_0 → step_2 relabeled "b (1→n)" via foreachEntry.
|
|
4274
|
+
// - step_2 → output merged because upstream.fullUrn equivalent
|
|
4275
|
+
// to target_spec (both media:c); step_2 takes the target
|
|
4276
|
+
// display label.
|
|
4277
|
+
//
|
|
4278
|
+
// Final: 3 nodes (input_slot, step_0, step_2), 2 edges.
|
|
4279
|
+
const payload = {
|
|
4280
|
+
source_spec: 'media:a',
|
|
4281
|
+
target_spec: 'media:c',
|
|
4282
|
+
steps: [
|
|
4283
|
+
makeCapStep('cap:in="media:a";op=a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4284
|
+
makeForEachStep('media:b'),
|
|
4285
|
+
makeCapStep('cap:in="media:b";op=b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
|
|
4286
|
+
],
|
|
4287
|
+
};
|
|
4288
|
+
const built = rendererBuildStrandGraphData(payload);
|
|
4289
|
+
const collapsed = rendererCollapseStrandShapeTransitions(built);
|
|
4290
|
+
|
|
4291
|
+
const nodeIds = collapsed.nodes.map(n => n.id).sort();
|
|
4292
|
+
assertEqual(JSON.stringify(nodeIds),
|
|
4293
|
+
JSON.stringify(['input_slot', 'step_0', 'step_2']),
|
|
4294
|
+
'foreach node removed and output merged into step_2 (same URN as target)');
|
|
4295
|
+
|
|
4296
|
+
// Exactly one edge from step_0 to step_2, labeled with cap_b's
|
|
4297
|
+
// title + (1→n) marker.
|
|
4298
|
+
const step0ToStep2 = collapsed.edges.filter(e => e.source === 'step_0' && e.target === 'step_2');
|
|
4299
|
+
assertEqual(step0ToStep2.length, 1,
|
|
4300
|
+
'exactly one step_0 → step_2 edge after dropping the foreach iteration');
|
|
4301
|
+
assertEqual(step0ToStep2[0].label, 'b (1\u2192n)',
|
|
4302
|
+
'the foreach-entry edge is labeled "<cap_b_title> (1→n)"');
|
|
4303
|
+
|
|
4304
|
+
// Cap_a's edge is unchanged (not inside a foreach body).
|
|
4305
|
+
const capA = collapsed.edges.find(e => e.source === 'input_slot' && e.target === 'step_0');
|
|
4306
|
+
assert(capA !== undefined, 'cap_a edge input_slot → step_0 exists');
|
|
4307
|
+
assertEqual(capA.label, 'a', 'cap_a edge carries just its title (no cardinality marker since 1→1)');
|
|
4308
|
+
|
|
4309
|
+
// After merging, step_2 becomes the render target — no separate
|
|
4310
|
+
// output node exists.
|
|
4311
|
+
const outputNode = collapsed.nodes.find(n => n.id === 'output');
|
|
4312
|
+
assertEqual(outputNode, undefined,
|
|
4313
|
+
'output node was merged into step_2 because their URNs are semantically equivalent');
|
|
4314
|
+
const mergedTarget = collapsed.nodes.find(n => n.id === 'step_2');
|
|
4315
|
+
assertEqual(mergedTarget.nodeClass, 'strand-target',
|
|
4316
|
+
'merged step_2 takes on the strand-target role');
|
|
4317
|
+
}
|
|
4318
|
+
|
|
4319
|
+
function testRenderer_collapseStrand_standaloneCollectCollapses() {
|
|
4320
|
+
// [Cap, Collect] with no enclosing ForEach, source=media:a,
|
|
4321
|
+
// target=media:b;list (NOT equivalent to cap's to_spec media:b,
|
|
4322
|
+
// so the output node is retained after collapse).
|
|
4323
|
+
//
|
|
4324
|
+
// Collapse:
|
|
4325
|
+
// - step_1 (standalone Collect) removed.
|
|
4326
|
+
// - Synthesized bridging edge step_0 → output labeled "collect".
|
|
4327
|
+
// - The cap edge input_slot → step_0 is unchanged because the
|
|
4328
|
+
// cap is not inside any foreach body.
|
|
4329
|
+
//
|
|
4330
|
+
// Final: 3 nodes (input_slot, step_0, output), 2 edges.
|
|
4331
|
+
const payload = {
|
|
4332
|
+
source_spec: 'media:a',
|
|
4333
|
+
target_spec: 'media:b;list',
|
|
4091
4334
|
steps: [
|
|
4092
4335
|
makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4336
|
+
makeCollectStep('media:b'),
|
|
4093
4337
|
],
|
|
4094
4338
|
};
|
|
4095
4339
|
const built = rendererBuildStrandGraphData(payload);
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4340
|
+
const collapsed = rendererCollapseStrandShapeTransitions(built);
|
|
4341
|
+
|
|
4342
|
+
const nodeIds = collapsed.nodes.map(n => n.id).sort();
|
|
4343
|
+
assertEqual(JSON.stringify(nodeIds),
|
|
4344
|
+
JSON.stringify(['input_slot', 'output', 'step_0']),
|
|
4345
|
+
'collect node removed; only cap + source + target remain');
|
|
4346
|
+
|
|
4347
|
+
const capEdge = collapsed.edges.find(e => e.source === 'input_slot' && e.target === 'step_0');
|
|
4348
|
+
assert(capEdge !== undefined, 'cap edge survives');
|
|
4349
|
+
assertEqual(capEdge.label, 'x',
|
|
4350
|
+
'cap edge carries just its title — no foreach cardinality markers because the cap is not inside a foreach body');
|
|
4351
|
+
|
|
4352
|
+
const collectEdge = collapsed.edges.find(e => e.source === 'step_0' && e.target === 'output');
|
|
4353
|
+
assert(collectEdge !== undefined, 'step_0 → output edge synthesized by collect collapse');
|
|
4354
|
+
assertEqual(collectEdge.label, 'collect',
|
|
4355
|
+
'the synthesized bridging edge for a standalone Collect is labeled "collect"');
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
function testRenderer_collapseStrand_sequenceProducingCapBeforeForeach() {
|
|
4359
|
+
// Regression test mirroring the user's real strand:
|
|
4360
|
+
// [Cap_disbind (output_is_sequence=true), ForEach, Cap_make_decision],
|
|
4361
|
+
// source = media:pdf, target = media:decision (equivalent to
|
|
4362
|
+
// the last cap's to_spec).
|
|
4363
|
+
//
|
|
4364
|
+
// Expected render shape after collapse:
|
|
4365
|
+
// input_slot → step_0 labeled "Disbind (1→n)" — from Disbind's
|
|
4366
|
+
// own output_is_sequence flag, computed at build time.
|
|
4367
|
+
// step_0 → step_2 labeled "Make a Decision (1→n)" — because
|
|
4368
|
+
// make_decision is the first cap inside an unclosed
|
|
4369
|
+
// ForEach body (foreachEntry=true).
|
|
4370
|
+
// No separate output node because step_2's to_spec equals the
|
|
4371
|
+
// strand target.
|
|
4372
|
+
//
|
|
4373
|
+
// If this test fails, the runtime bug would manifest as either
|
|
4374
|
+
// (a) a duplicate target node, (b) a "for each" labeled edge
|
|
4375
|
+
// where the cap title should be, or (c) the phantom direct cap
|
|
4376
|
+
// edge not being relabeled.
|
|
4102
4377
|
const payload = {
|
|
4103
|
-
source_spec: 'media:
|
|
4378
|
+
source_spec: 'media:pdf',
|
|
4379
|
+
target_spec: 'media:decision',
|
|
4380
|
+
steps: [
|
|
4381
|
+
makeCapStep('cap:in="media:pdf";op=disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
|
|
4382
|
+
makeForEachStep('media:page'),
|
|
4383
|
+
makeCapStep('cap:in="media:page";op=decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
4384
|
+
],
|
|
4385
|
+
};
|
|
4386
|
+
const built = rendererBuildStrandGraphData(payload);
|
|
4387
|
+
const collapsed = rendererCollapseStrandShapeTransitions(built);
|
|
4388
|
+
|
|
4389
|
+
const nodeIds = collapsed.nodes.map(n => n.id).sort();
|
|
4390
|
+
assertEqual(JSON.stringify(nodeIds),
|
|
4391
|
+
JSON.stringify(['input_slot', 'step_0', 'step_2']),
|
|
4392
|
+
'foreach node and duplicate output node both removed');
|
|
4393
|
+
|
|
4394
|
+
// Disbind cap edge carries its own (1→n) marker from
|
|
4395
|
+
// output_is_sequence=true, NOT from the foreach flag.
|
|
4396
|
+
const disbind = collapsed.edges.find(e => e.source === 'input_slot' && e.target === 'step_0');
|
|
4397
|
+
assert(disbind !== undefined, 'Disbind edge input_slot → step_0 exists');
|
|
4398
|
+
assertEqual(disbind.label, 'Disbind (1\u2192n)',
|
|
4399
|
+
'Disbind edge reflects its own output_is_sequence=true cardinality');
|
|
4400
|
+
|
|
4401
|
+
// make_decision cap edge is the foreach entry — the plan-builder
|
|
4402
|
+
// phantom direct edge becomes the render-visible cap edge with
|
|
4403
|
+
// (1→n) appended to the cap title.
|
|
4404
|
+
const makeDecision = collapsed.edges.filter(e => e.source === 'step_0' && e.target === 'step_2');
|
|
4405
|
+
assertEqual(makeDecision.length, 1,
|
|
4406
|
+
'exactly one edge from Text Page to Decision (phantom not duplicated)');
|
|
4407
|
+
assertEqual(makeDecision[0].label, 'Make a Decision (1\u2192n)',
|
|
4408
|
+
'the foreach entry edge is labeled "<cap_title> (1→n)", not "for each"');
|
|
4409
|
+
|
|
4410
|
+
// Duplicate target must be gone.
|
|
4411
|
+
const outputNode = collapsed.nodes.find(n => n.id === 'output');
|
|
4412
|
+
assertEqual(outputNode, undefined,
|
|
4413
|
+
'output node merged into step_2 because they represent the same URN');
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
function testRenderer_collapseStrand_plainCapMergesTrailingOutput() {
|
|
4417
|
+
// A strand with a single plain 1→1 cap whose to_spec equals
|
|
4418
|
+
// target_spec. The plan-builder topology produces:
|
|
4419
|
+
// input_slot → step_0 (cap) → output
|
|
4420
|
+
// The collapse pass merges the trailing output edge because
|
|
4421
|
+
// step_0 and output represent the same URN (media:b).
|
|
4422
|
+
//
|
|
4423
|
+
// Final: 2 nodes (input_slot, step_0), 1 edge.
|
|
4424
|
+
const payload = {
|
|
4425
|
+
source_spec: 'media:a',
|
|
4104
4426
|
target_spec: 'media:b',
|
|
4105
4427
|
steps: [
|
|
4106
|
-
makeCapStep('cap:in="media:a
|
|
4428
|
+
makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4107
4429
|
],
|
|
4108
4430
|
};
|
|
4109
4431
|
const built = rendererBuildStrandGraphData(payload);
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4432
|
+
const collapsed = rendererCollapseStrandShapeTransitions(built);
|
|
4433
|
+
|
|
4434
|
+
assertEqual(collapsed.nodes.length, 2,
|
|
4435
|
+
'duplicate output node merged into step_0 — 2 nodes remain');
|
|
4436
|
+
const outputNode = collapsed.nodes.find(n => n.id === 'output');
|
|
4437
|
+
assertEqual(outputNode, undefined,
|
|
4438
|
+
'output node dropped by merge');
|
|
4439
|
+
const mergedTarget = collapsed.nodes.find(n => n.id === 'step_0');
|
|
4440
|
+
assertEqual(mergedTarget.nodeClass, 'strand-target',
|
|
4441
|
+
'step_0 takes on the strand-target role after the merge');
|
|
4442
|
+
|
|
4443
|
+
assertEqual(collapsed.edges.length, 1, 'single cap edge remains');
|
|
4444
|
+
assertEqual(collapsed.edges[0].source, 'input_slot');
|
|
4445
|
+
assertEqual(collapsed.edges[0].target, 'step_0');
|
|
4446
|
+
assertEqual(collapsed.edges[0].label, 'x', 'cap title preserved as edge label');
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
function testRenderer_collapseStrand_plainCapDistinctTargetNoMerge() {
|
|
4450
|
+
// A strand with a single plain cap whose to_spec is NOT
|
|
4451
|
+
// equivalent to target_spec. The output node must be retained
|
|
4452
|
+
// and the trailing connector edge preserved.
|
|
4453
|
+
const payload = {
|
|
4454
|
+
source_spec: 'media:a',
|
|
4455
|
+
target_spec: 'media:b;list',
|
|
4456
|
+
steps: [
|
|
4457
|
+
makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4458
|
+
],
|
|
4459
|
+
};
|
|
4460
|
+
const built = rendererBuildStrandGraphData(payload);
|
|
4461
|
+
const collapsed = rendererCollapseStrandShapeTransitions(built);
|
|
4462
|
+
|
|
4463
|
+
assertEqual(collapsed.nodes.length, 3,
|
|
4464
|
+
'no merge because cap to_spec (media:b) and target (media:b;list) are semantically distinct');
|
|
4465
|
+
assert(collapsed.nodes.find(n => n.id === 'output') !== undefined,
|
|
4466
|
+
'output node retained');
|
|
4467
|
+
assert(collapsed.nodes.find(n => n.id === 'step_0') !== undefined,
|
|
4468
|
+
'step_0 retained');
|
|
4113
4469
|
}
|
|
4114
4470
|
|
|
4115
4471
|
function testRenderer_validateStrandPayload_missingSourceSpec() {
|
|
@@ -4280,6 +4636,156 @@ function testRenderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap() {
|
|
|
4280
4636
|
assertEqual(failureNodes, 2, 'trace truncates at cap y via isEquivalent, yielding 2 failure nodes');
|
|
4281
4637
|
}
|
|
4282
4638
|
|
|
4639
|
+
function testRenderer_buildRunGraphData_backboneHasNoForeachNode() {
|
|
4640
|
+
// Regression test for the run-mode rendering fix: the backbone
|
|
4641
|
+
// delivered to cytoscape must NOT contain any strand-foreach or
|
|
4642
|
+
// strand-collect nodes. Run mode inherits the same cosmetic
|
|
4643
|
+
// collapse as strand mode so the foreach/collect execution-layer
|
|
4644
|
+
// concepts don't leak into the view as boxed nodes.
|
|
4645
|
+
//
|
|
4646
|
+
// User scenario: [Disbind (1→n), ForEach, make_decision] where
|
|
4647
|
+
// target_spec equals the last cap's to_spec, so the backbone
|
|
4648
|
+
// collapses to 3 nodes: input_slot, step_0 (Text Page),
|
|
4649
|
+
// step_2 (Decision, merged target). No separate `for each` or
|
|
4650
|
+
// `collect` boxes.
|
|
4651
|
+
const strand = {
|
|
4652
|
+
source_spec: 'media:pdf',
|
|
4653
|
+
target_spec: 'media:decision',
|
|
4654
|
+
steps: [
|
|
4655
|
+
makeCapStep('cap:in="media:pdf";op=disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
|
|
4656
|
+
makeForEachStep('media:page'),
|
|
4657
|
+
makeCapStep('cap:in="media:page";op=decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
4658
|
+
],
|
|
4659
|
+
};
|
|
4660
|
+
const payload = {
|
|
4661
|
+
resolved_strand: strand,
|
|
4662
|
+
body_outcomes: [],
|
|
4663
|
+
visible_success_count: 0,
|
|
4664
|
+
visible_failure_count: 0,
|
|
4665
|
+
total_body_count: 0,
|
|
4666
|
+
};
|
|
4667
|
+
const built = rendererBuildRunGraphData(payload);
|
|
4668
|
+
|
|
4669
|
+
// Backbone must contain NO foreach/collect nodes.
|
|
4670
|
+
const foreachNodes = built.strandBuilt.nodes.filter(n => n.nodeClass === 'strand-foreach');
|
|
4671
|
+
const collectNodes = built.strandBuilt.nodes.filter(n => n.nodeClass === 'strand-collect');
|
|
4672
|
+
assertEqual(foreachNodes.length, 0, 'run backbone must not contain strand-foreach nodes');
|
|
4673
|
+
assertEqual(collectNodes.length, 0, 'run backbone must not contain strand-collect nodes');
|
|
4674
|
+
|
|
4675
|
+
// The backbone fallback connector is the foreach-entry cap edge
|
|
4676
|
+
// that runs from the pre-foreach node to the body cap. It must
|
|
4677
|
+
// survive collapse so the target stays reachable even with zero
|
|
4678
|
+
// successful bodies.
|
|
4679
|
+
const backboneCapEdges = built.strandBuilt.edges.filter(e => e.edgeClass === 'strand-cap-edge');
|
|
4680
|
+
assert(backboneCapEdges.some(e => e.source === 'step_0' && e.target === 'step_2'),
|
|
4681
|
+
'foreach-entry backbone edge step_0 → step_2 must be present for fallback connectivity');
|
|
4682
|
+
|
|
4683
|
+
// With zero outcomes, no replicas and no show-more nodes.
|
|
4684
|
+
assertEqual(built.replicaNodes.length, 0, 'no replica nodes when body_outcomes is empty');
|
|
4685
|
+
assertEqual(built.showMoreNodes.length, 0, 'no show-more nodes when no hidden outcomes');
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
function testRenderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
|
|
4689
|
+
// When every body fails, the strand target node was never
|
|
4690
|
+
// reached by any execution. The render drops BOTH the backbone
|
|
4691
|
+
// foreach-entry edge AND the orphaned target node so the user
|
|
4692
|
+
// doesn't see a stale "Decision" placeholder alongside their
|
|
4693
|
+
// failed replicas.
|
|
4694
|
+
const strand = {
|
|
4695
|
+
source_spec: 'media:pdf',
|
|
4696
|
+
target_spec: 'media:decision',
|
|
4697
|
+
steps: [
|
|
4698
|
+
makeCapStep('cap:in="media:pdf";op=disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
|
|
4699
|
+
makeForEachStep('media:page'),
|
|
4700
|
+
makeCapStep('cap:in="media:page";op=decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
4701
|
+
],
|
|
4702
|
+
};
|
|
4703
|
+
const failedCapUrn = 'cap:in="media:page";op=decide;out="media:decision"';
|
|
4704
|
+
const payload = {
|
|
4705
|
+
resolved_strand: strand,
|
|
4706
|
+
body_outcomes: [
|
|
4707
|
+
{ body_index: 0, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_cap: failedCapUrn, error: 'boom' },
|
|
4708
|
+
{ body_index: 1, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_cap: failedCapUrn, error: 'boom' },
|
|
4709
|
+
],
|
|
4710
|
+
visible_success_count: 3,
|
|
4711
|
+
visible_failure_count: 3,
|
|
4712
|
+
total_body_count: 2,
|
|
4713
|
+
};
|
|
4714
|
+
const built = rendererBuildRunGraphData(payload);
|
|
4715
|
+
|
|
4716
|
+
// The dropped placeholder: step_2 (the merged strand target
|
|
4717
|
+
// "Decision") is absent from the backbone because all bodies
|
|
4718
|
+
// failed and the replicas didn't reach it.
|
|
4719
|
+
const hasStep2 = built.strandBuilt.nodes.some(n => n.id === 'step_2');
|
|
4720
|
+
assertEqual(hasStep2, false,
|
|
4721
|
+
'strand target placeholder must be dropped when zero successful replicas reach it');
|
|
4722
|
+
|
|
4723
|
+
// The backbone foreach-entry edge is also gone — replicas
|
|
4724
|
+
// replaced it and there's no orphan target to connect.
|
|
4725
|
+
const foreachEntry = built.strandBuilt.edges.find(e =>
|
|
4726
|
+
e.edgeClass === 'strand-cap-edge' && e.foreachEntry === true);
|
|
4727
|
+
assertEqual(foreachEntry, undefined,
|
|
4728
|
+
'backbone foreach-entry edge must be dropped when replicas exist');
|
|
4729
|
+
|
|
4730
|
+
// Failures are rendered as red replica trails that don't merge.
|
|
4731
|
+
const failureNodes = built.replicaNodes.filter(n => n.classes === 'body-failure');
|
|
4732
|
+
assertEqual(failureNodes.length, 2,
|
|
4733
|
+
'two failed bodies render two replica nodes (truncated at failed_cap)');
|
|
4734
|
+
|
|
4735
|
+
// The pre-foreach cap node (step_0, "Disbind") is still present
|
|
4736
|
+
// along with its incoming edge — only the post-body parts were
|
|
4737
|
+
// dropped.
|
|
4738
|
+
const hasStep0 = built.strandBuilt.nodes.some(n => n.id === 'step_0');
|
|
4739
|
+
assertEqual(hasStep0, true, 'pre-foreach cap node survives');
|
|
4740
|
+
const disbindEdge = built.strandBuilt.edges.find(e =>
|
|
4741
|
+
e.source === 'input_slot' && e.target === 'step_0');
|
|
4742
|
+
assert(disbindEdge !== undefined, 'pre-foreach cap edge (Disbind) survives');
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
function testRenderer_buildRunGraphData_backboneDroppedWhenSuccessful() {
|
|
4746
|
+
// When at least one successful replica merges into the target,
|
|
4747
|
+
// the backbone foreach-entry edge is dropped — the replica's
|
|
4748
|
+
// own chain represents the execution and the backbone would
|
|
4749
|
+
// otherwise duplicate the path.
|
|
4750
|
+
const strand = {
|
|
4751
|
+
source_spec: 'media:pdf',
|
|
4752
|
+
target_spec: 'media:decision',
|
|
4753
|
+
steps: [
|
|
4754
|
+
makeCapStep('cap:in="media:pdf";op=disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
|
|
4755
|
+
makeForEachStep('media:page'),
|
|
4756
|
+
makeCapStep('cap:in="media:page";op=decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
|
|
4757
|
+
],
|
|
4758
|
+
};
|
|
4759
|
+
const payload = {
|
|
4760
|
+
resolved_strand: strand,
|
|
4761
|
+
body_outcomes: [
|
|
4762
|
+
{ body_index: 0, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0 },
|
|
4763
|
+
],
|
|
4764
|
+
visible_success_count: 3,
|
|
4765
|
+
visible_failure_count: 3,
|
|
4766
|
+
total_body_count: 1,
|
|
4767
|
+
};
|
|
4768
|
+
const built = rendererBuildRunGraphData(payload);
|
|
4769
|
+
|
|
4770
|
+
// The backbone foreach-entry edge is gone.
|
|
4771
|
+
const foreachEntry = built.strandBuilt.edges.find(e =>
|
|
4772
|
+
e.edgeClass === 'strand-cap-edge' && e.foreachEntry === true);
|
|
4773
|
+
assertEqual(foreachEntry, undefined,
|
|
4774
|
+
'foreach-entry backbone edge dropped when at least one success exists');
|
|
4775
|
+
|
|
4776
|
+
// step_2 (target) stays because the replica merge edge lands on it.
|
|
4777
|
+
const hasStep2 = built.strandBuilt.nodes.some(n => n.id === 'step_2');
|
|
4778
|
+
assertEqual(hasStep2, true,
|
|
4779
|
+
'strand target node survives when successful replicas merge into it');
|
|
4780
|
+
|
|
4781
|
+
// Exactly one successful replica node and one merge edge.
|
|
4782
|
+
const successNodes = built.replicaNodes.filter(n => n.classes === 'body-success');
|
|
4783
|
+
assertEqual(successNodes.length, 1, 'one replica node for one successful body');
|
|
4784
|
+
const mergeEdges = built.replicaEdges.filter(e =>
|
|
4785
|
+
e.data && e.data.target === 'step_2' && e.classes === 'body-success');
|
|
4786
|
+
assertEqual(mergeEdges.length, 1, 'one merge edge from replica to target');
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4283
4789
|
// ---------------- machine builder ----------------
|
|
4284
4790
|
|
|
4285
4791
|
function testRenderer_validateMachinePayload_rejectsUnknownKind() {
|
|
@@ -4702,10 +5208,18 @@ async function runTests() {
|
|
|
4702
5208
|
runTest('RENDERER: validateStrandStep_booleanIsSequence', testRenderer_validateStrandStep_requiresBooleanIsSequence);
|
|
4703
5209
|
runTest('RENDERER: classifyStrandCapSteps_simple', testRenderer_classifyStrandCapSteps_capFlags);
|
|
4704
5210
|
runTest('RENDERER: classifyStrandCapSteps_nested', testRenderer_classifyStrandCapSteps_nestedForks);
|
|
4705
|
-
runTest('RENDERER:
|
|
4706
|
-
runTest('RENDERER: buildStrand_collectFixupEdge', testRenderer_buildStrandGraphData_collectFixupEdge);
|
|
4707
|
-
runTest('RENDERER: buildStrand_plainCapNoMarker', testRenderer_buildStrandGraphData_plainCapNoMarker);
|
|
5211
|
+
runTest('RENDERER: buildStrand_singleCapPlain', testRenderer_buildStrandGraphData_singleCapPlain);
|
|
4708
5212
|
runTest('RENDERER: buildStrand_sequenceShowsCardinality', testRenderer_buildStrandGraphData_sequenceShowsCardinality);
|
|
5213
|
+
runTest('RENDERER: buildStrand_foreachCollectSpan', testRenderer_buildStrandGraphData_foreachCollectSpan);
|
|
5214
|
+
runTest('RENDERER: buildStrand_standaloneCollect', testRenderer_buildStrandGraphData_standaloneCollect);
|
|
5215
|
+
runTest('RENDERER: buildStrand_unclosedForEachBody', testRenderer_buildStrandGraphData_unclosedForEachBody);
|
|
5216
|
+
runTest('RENDERER: buildStrand_nestedForEachThrows', testRenderer_buildStrandGraphData_nestedForEachThrows);
|
|
5217
|
+
runTest('RENDERER: collapseStrand_singleCapBody', testRenderer_collapseStrand_singleCapBodyShowsCapTitleWithIterCollectMarker);
|
|
5218
|
+
runTest('RENDERER: collapseStrand_unclosedForEachBody', testRenderer_collapseStrand_unclosedForEachBodyCollapses);
|
|
5219
|
+
runTest('RENDERER: collapseStrand_standaloneCollect', testRenderer_collapseStrand_standaloneCollectCollapses);
|
|
5220
|
+
runTest('RENDERER: collapseStrand_seqCapBeforeForeach', testRenderer_collapseStrand_sequenceProducingCapBeforeForeach);
|
|
5221
|
+
runTest('RENDERER: collapseStrand_plainCapMergesOutput', testRenderer_collapseStrand_plainCapMergesTrailingOutput);
|
|
5222
|
+
runTest('RENDERER: collapseStrand_plainCapDistinctTarget', testRenderer_collapseStrand_plainCapDistinctTargetNoMerge);
|
|
4709
5223
|
runTest('RENDERER: validateStrand_missingSourceSpec', testRenderer_validateStrandPayload_missingSourceSpec);
|
|
4710
5224
|
|
|
4711
5225
|
console.log('\n--- cap-graph-renderer run builder ---');
|
|
@@ -4713,6 +5227,9 @@ async function runTests() {
|
|
|
4713
5227
|
runTest('RENDERER: buildRun_pagesSuccessesAndFailures', testRenderer_buildRunGraphData_pagesSuccessesAndFailures);
|
|
4714
5228
|
runTest('RENDERER: buildRun_failureWithoutFailedCap', testRenderer_buildRunGraphData_failureWithoutFailedCapRendersFullTrace);
|
|
4715
5229
|
runTest('RENDERER: buildRun_usesIsEquivalentForFailedCap', testRenderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap);
|
|
5230
|
+
runTest('RENDERER: buildRun_backboneHasNoForeachNode', testRenderer_buildRunGraphData_backboneHasNoForeachNode);
|
|
5231
|
+
runTest('RENDERER: buildRun_allFailedDropsPlaceholder', testRenderer_buildRunGraphData_allFailedDropsTargetPlaceholder);
|
|
5232
|
+
runTest('RENDERER: buildRun_backboneDroppedWhenSuccessful', testRenderer_buildRunGraphData_backboneDroppedWhenSuccessful);
|
|
4716
5233
|
|
|
4717
5234
|
console.log('\n--- cap-graph-renderer machine builder ---');
|
|
4718
5235
|
runTest('RENDERER: validateMachine_unknownKind', testRenderer_validateMachinePayload_rejectsUnknownKind);
|