capdag 0.183.463 → 0.186.476
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/RULES.md +8 -8
- package/build-browser.js +3 -3
- package/cap-fab-renderer.js +44 -33
- package/capdag.js +198 -188
- package/capdag.test.js +153 -149
- package/package.json +1 -1
package/capdag.test.js
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
const {
|
|
6
6
|
CapUrn, CapKind, CapEffect, CapUrnBuilder, CapMatcher, CapUrnError, ErrorCodes,
|
|
7
7
|
MediaUrn, MediaUrnError, MediaUrnErrorCodes,
|
|
8
|
-
Cap, CapGroup, CapManifest,
|
|
8
|
+
Cap, CapGroup, CapManifest, MediaDef, MediaDefError, MediaDefErrorCodes,
|
|
9
9
|
resolveMediaUrn, buildExtensionIndex, mediaUrnsForExtension, getExtensionMappings,
|
|
10
10
|
CartridgeInfo, CartridgeCapSummary, CartridgeSuggestion, CartridgeRepoClient, CartridgeRepoServer,
|
|
11
11
|
CapFabEdge, CapFabStats, CapFab,
|
|
12
12
|
StdinSource, StdinSourceKind,
|
|
13
|
-
|
|
13
|
+
validateNoMediaDefRedefinitionSync,
|
|
14
14
|
CapArgumentValue, CapArg, ArgSource, validateCapArgs, ValidationError,
|
|
15
15
|
llmGenerateTextUrn, modelAvailabilityUrn, modelPathUrn,
|
|
16
16
|
MachineSyntaxError, MachineSyntaxErrorCodes, MachineEdge, Machine, MachineBuilder, parseMachine, parseMachineWithAST,
|
|
@@ -318,7 +318,7 @@ function test939_capUrnCanonicalFormDropsWildcardInOut() {
|
|
|
318
318
|
|
|
319
319
|
const identity = CapUrn.fromString('cap:effect=none');
|
|
320
320
|
assertEqual(identity.toString(), 'cap:effect=none', 'true identity must preserve explicit effect=none');
|
|
321
|
-
assert(identity.toString() !==
|
|
321
|
+
assert(identity.toString() !== 'cap:', 'cap:effect=none must not collapse to the illegal bare top form');
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
// TEST017: Test tag matching: exact match, subset match, wildcard match, value mismatch
|
|
@@ -692,9 +692,9 @@ function test047_matchingSemanticsThumbnailVoidInput() {
|
|
|
692
692
|
|
|
693
693
|
// TEST048: Matching semantics - wildcard direction matches anything
|
|
694
694
|
function test048_matchingSemanticsWildcardDirection() {
|
|
695
|
-
const cap = CapUrn.fromString('cap:
|
|
695
|
+
const cap = CapUrn.fromString('cap:generate');
|
|
696
696
|
const request = CapUrn.fromString(testUrn('generate;ext=pdf'));
|
|
697
|
-
assert(cap.accepts(request), '
|
|
697
|
+
assert(cap.accepts(request), 'Generic declared directions should accept a more specific matching request');
|
|
698
698
|
}
|
|
699
699
|
|
|
700
700
|
// TEST049: Non-overlapping tags — neither direction accepts
|
|
@@ -809,10 +809,10 @@ function test891_directionSemanticSpecificity() {
|
|
|
809
809
|
|
|
810
810
|
// TEST053: N/A for JS (Rust-only validation infrastructure)
|
|
811
811
|
|
|
812
|
-
// TEST054: XV5 - Test inline media
|
|
812
|
+
// TEST054: XV5 - Test inline media def redefinition of existing registry spec is detected and rejected
|
|
813
813
|
function test054_xv5InlineSpecRedefinitionDetected() {
|
|
814
814
|
const registryLookup = (mediaUrn) => mediaUrn === MEDIA_STRING;
|
|
815
|
-
const
|
|
815
|
+
const mediaDefs = [
|
|
816
816
|
{
|
|
817
817
|
urn: MEDIA_STRING,
|
|
818
818
|
media_type: 'text/plain',
|
|
@@ -820,16 +820,16 @@ function test054_xv5InlineSpecRedefinitionDetected() {
|
|
|
820
820
|
description: 'Trying to redefine string'
|
|
821
821
|
}
|
|
822
822
|
];
|
|
823
|
-
const result =
|
|
823
|
+
const result = validateNoMediaDefRedefinitionSync(mediaDefs, registryLookup);
|
|
824
824
|
assert(!result.valid, 'Should fail when redefining registry spec');
|
|
825
825
|
assert(result.error && result.error.includes('XV5'), 'Error should mention XV5');
|
|
826
826
|
assert(result.redefines && result.redefines.includes(MEDIA_STRING), 'Should identify MEDIA_STRING as redefined');
|
|
827
827
|
}
|
|
828
828
|
|
|
829
|
-
// TEST055: XV5 - Test new inline media
|
|
829
|
+
// TEST055: XV5 - Test new inline media def (not in registry) is allowed
|
|
830
830
|
function test055_xv5NewInlineSpecAllowed() {
|
|
831
831
|
const registryLookup = (mediaUrn) => mediaUrn === MEDIA_STRING;
|
|
832
|
-
const
|
|
832
|
+
const mediaDefs = [
|
|
833
833
|
{
|
|
834
834
|
urn: 'media:my-unique-custom-type-xyz123',
|
|
835
835
|
media_type: 'application/json',
|
|
@@ -837,16 +837,16 @@ function test055_xv5NewInlineSpecAllowed() {
|
|
|
837
837
|
description: 'A custom output type'
|
|
838
838
|
}
|
|
839
839
|
];
|
|
840
|
-
const result =
|
|
840
|
+
const result = validateNoMediaDefRedefinitionSync(mediaDefs, registryLookup);
|
|
841
841
|
assert(result.valid, 'New spec not in registry should pass validation');
|
|
842
842
|
}
|
|
843
843
|
|
|
844
|
-
// TEST056: XV5 - Test empty
|
|
845
|
-
function
|
|
844
|
+
// TEST056: XV5 - Test empty media_defs (no inline specs) passes XV5 validation
|
|
845
|
+
function test056_xv5EmptyMediaDefsAllowed() {
|
|
846
846
|
const registryLookup = (mediaUrn) => mediaUrn === MEDIA_STRING;
|
|
847
|
-
assert(
|
|
848
|
-
assert(
|
|
849
|
-
assert(
|
|
847
|
+
assert(validateNoMediaDefRedefinitionSync({}, registryLookup).valid, 'Empty object should pass');
|
|
848
|
+
assert(validateNoMediaDefRedefinitionSync(null, registryLookup).valid, 'Null should pass');
|
|
849
|
+
assert(validateNoMediaDefRedefinitionSync(undefined, registryLookup).valid, 'Undefined should pass');
|
|
850
850
|
}
|
|
851
851
|
|
|
852
852
|
// ============================================================================
|
|
@@ -1028,26 +1028,26 @@ function test078_debugMatchingBehavior() {
|
|
|
1028
1028
|
}
|
|
1029
1029
|
|
|
1030
1030
|
// ============================================================================
|
|
1031
|
-
//
|
|
1031
|
+
// media_def.rs: TEST088-TEST110
|
|
1032
1032
|
// ============================================================================
|
|
1033
1033
|
|
|
1034
1034
|
// TEST088: N/A for JS (async registry, Rust-only)
|
|
1035
1035
|
// TEST089: N/A for JS
|
|
1036
1036
|
// TEST090: N/A for JS
|
|
1037
1037
|
|
|
1038
|
-
// TEST091: Test resolving custom media URN from local
|
|
1039
|
-
function
|
|
1040
|
-
const
|
|
1038
|
+
// TEST091: Test resolving custom media URN from local media_defs takes precedence over registry
|
|
1039
|
+
function test091_resolveCustomMediaDef() {
|
|
1040
|
+
const mediaDefs = [
|
|
1041
1041
|
{ urn: 'media:custom-json', media_type: 'application/json', title: 'Custom JSON', profile_uri: 'https://example.com/schema/custom' }
|
|
1042
1042
|
];
|
|
1043
|
-
const spec = resolveMediaUrn('media:custom-json',
|
|
1043
|
+
const spec = resolveMediaUrn('media:custom-json', mediaDefs);
|
|
1044
1044
|
assertEqual(spec.contentType, 'application/json', 'Should resolve custom spec');
|
|
1045
1045
|
assertEqual(spec.profile, 'https://example.com/schema/custom', 'Should have custom profile');
|
|
1046
1046
|
}
|
|
1047
1047
|
|
|
1048
|
-
// TEST092: Test resolving custom record media
|
|
1048
|
+
// TEST092: Test resolving custom record media def with schema from local media_defs
|
|
1049
1049
|
function test092_resolveCustomWithSchema() {
|
|
1050
|
-
const
|
|
1050
|
+
const mediaDefs = [
|
|
1051
1051
|
{
|
|
1052
1052
|
urn: 'media:rich-xml',
|
|
1053
1053
|
media_type: 'application/xml',
|
|
@@ -1056,7 +1056,7 @@ function test092_resolveCustomWithSchema() {
|
|
|
1056
1056
|
schema: { type: 'object' }
|
|
1057
1057
|
}
|
|
1058
1058
|
];
|
|
1059
|
-
const spec = resolveMediaUrn('media:rich-xml',
|
|
1059
|
+
const spec = resolveMediaUrn('media:rich-xml', mediaDefs);
|
|
1060
1060
|
assertEqual(spec.contentType, 'application/xml', 'Should resolve rich spec');
|
|
1061
1061
|
assert(spec.schema !== null, 'Should have schema');
|
|
1062
1062
|
assertEqual(spec.schema.type, 'object', 'Schema should have correct type');
|
|
@@ -1068,7 +1068,7 @@ function test093_resolveUnresolvableFailsHard() {
|
|
|
1068
1068
|
try {
|
|
1069
1069
|
resolveMediaUrn('media:nonexistent', []);
|
|
1070
1070
|
} catch (e) {
|
|
1071
|
-
if (e instanceof
|
|
1071
|
+
if (e instanceof MediaDefError && e.code === MediaDefErrorCodes.UNRESOLVABLE_MEDIA_URN) {
|
|
1072
1072
|
caught = true;
|
|
1073
1073
|
}
|
|
1074
1074
|
}
|
|
@@ -1081,45 +1081,45 @@ function test093_resolveUnresolvableFailsHard() {
|
|
|
1081
1081
|
// TEST097: N/A for JS (Rust validation function)
|
|
1082
1082
|
// TEST098: N/A for JS
|
|
1083
1083
|
|
|
1084
|
-
// TEST099: Test
|
|
1084
|
+
// TEST099: Test ResolvedMediaDef is_binary returns true when textable tag is absent
|
|
1085
1085
|
function test099_resolvedIsBinary() {
|
|
1086
|
-
const spec = new
|
|
1086
|
+
const spec = new MediaDef('application/octet-stream', null, null, 'Binary', null, MEDIA_IDENTITY);
|
|
1087
1087
|
assert(spec.isBinary(), 'Resolved binary spec should be binary');
|
|
1088
1088
|
}
|
|
1089
1089
|
|
|
1090
|
-
// TEST100: Test
|
|
1090
|
+
// TEST100: Test ResolvedMediaDef is_record returns true when record marker is present
|
|
1091
1091
|
function test100_resolvedIsRecord() {
|
|
1092
|
-
const spec = new
|
|
1092
|
+
const spec = new MediaDef('application/json', null, null, 'Object', null, MEDIA_OBJECT);
|
|
1093
1093
|
assert(spec.isRecord(), 'Resolved object spec should be record');
|
|
1094
1094
|
}
|
|
1095
1095
|
|
|
1096
|
-
// TEST101: Test
|
|
1096
|
+
// TEST101: Test ResolvedMediaDef is_scalar returns true when list marker is absent
|
|
1097
1097
|
function test101_resolvedIsScalar() {
|
|
1098
|
-
const spec = new
|
|
1098
|
+
const spec = new MediaDef('text/plain', null, null, 'String', null, MEDIA_STRING);
|
|
1099
1099
|
assert(spec.isScalar(), 'Resolved string spec should be scalar');
|
|
1100
1100
|
}
|
|
1101
1101
|
|
|
1102
|
-
// TEST102: Test
|
|
1102
|
+
// TEST102: Test ResolvedMediaDef is_list returns true when list marker is present
|
|
1103
1103
|
function test102_resolvedIsList() {
|
|
1104
|
-
const spec = new
|
|
1104
|
+
const spec = new MediaDef('text/plain', null, null, 'String List', null, MEDIA_STRING_LIST);
|
|
1105
1105
|
assert(spec.isList(), 'Resolved string_list spec should be list');
|
|
1106
1106
|
}
|
|
1107
1107
|
|
|
1108
|
-
// TEST103: Test
|
|
1108
|
+
// TEST103: Test ResolvedMediaDef is_json returns true when json tag is present
|
|
1109
1109
|
function test103_resolvedIsJson() {
|
|
1110
|
-
const spec = new
|
|
1110
|
+
const spec = new MediaDef('application/json', null, null, 'JSON', null, MEDIA_JSON);
|
|
1111
1111
|
assert(spec.isJSON(), 'Resolved json spec should be JSON');
|
|
1112
1112
|
}
|
|
1113
1113
|
|
|
1114
|
-
// TEST104: Test
|
|
1114
|
+
// TEST104: Test ResolvedMediaDef is_text returns true when textable tag is present
|
|
1115
1115
|
function test104_resolvedIsText() {
|
|
1116
|
-
const spec = new
|
|
1116
|
+
const spec = new MediaDef('text/plain', null, null, 'String', null, MEDIA_STRING);
|
|
1117
1117
|
assert(spec.isText(), 'Resolved string spec should be text');
|
|
1118
1118
|
}
|
|
1119
1119
|
|
|
1120
|
-
// TEST105: Test metadata propagates from media
|
|
1120
|
+
// TEST105: Test metadata propagates from media def def to resolved media def
|
|
1121
1121
|
function test105_metadataPropagation() {
|
|
1122
|
-
const
|
|
1122
|
+
const mediaDefs = [
|
|
1123
1123
|
{
|
|
1124
1124
|
urn: 'media:custom-setting',
|
|
1125
1125
|
media_type: 'text/plain',
|
|
@@ -1133,16 +1133,16 @@ function test105_metadataPropagation() {
|
|
|
1133
1133
|
}
|
|
1134
1134
|
}
|
|
1135
1135
|
];
|
|
1136
|
-
const resolved = resolveMediaUrn('media:custom-setting',
|
|
1136
|
+
const resolved = resolveMediaUrn('media:custom-setting', mediaDefs);
|
|
1137
1137
|
assert(resolved.metadata !== null, 'Should have metadata');
|
|
1138
1138
|
assertEqual(resolved.metadata.category_key, 'interface', 'Should propagate category_key');
|
|
1139
1139
|
assertEqual(resolved.metadata.ui_type, 'SETTING_UI_TYPE_CHECKBOX', 'Should propagate ui_type');
|
|
1140
1140
|
assertEqual(resolved.metadata.display_index, 5, 'Should propagate display_index');
|
|
1141
1141
|
}
|
|
1142
1142
|
|
|
1143
|
-
// TEST106: Test metadata and validation can coexist in media
|
|
1143
|
+
// TEST106: Test metadata and validation can coexist in media definition
|
|
1144
1144
|
function test106_metadataWithValidation() {
|
|
1145
|
-
const
|
|
1145
|
+
const mediaDefs = [
|
|
1146
1146
|
{
|
|
1147
1147
|
urn: 'media:bounded-number;numeric',
|
|
1148
1148
|
media_type: 'text/plain',
|
|
@@ -1151,7 +1151,7 @@ function test106_metadataWithValidation() {
|
|
|
1151
1151
|
metadata: { category_key: 'inference', ui_type: 'SETTING_UI_TYPE_SLIDER' }
|
|
1152
1152
|
}
|
|
1153
1153
|
];
|
|
1154
|
-
const resolved = resolveMediaUrn('media:bounded-number;numeric',
|
|
1154
|
+
const resolved = resolveMediaUrn('media:bounded-number;numeric', mediaDefs);
|
|
1155
1155
|
assert(resolved.validation !== null, 'Should have validation');
|
|
1156
1156
|
assertEqual(resolved.validation.min, 0, 'Should have min');
|
|
1157
1157
|
assertEqual(resolved.validation.max, 100, 'Should have max');
|
|
@@ -1159,9 +1159,9 @@ function test106_metadataWithValidation() {
|
|
|
1159
1159
|
assertEqual(resolved.metadata.category_key, 'inference', 'Should have category_key');
|
|
1160
1160
|
}
|
|
1161
1161
|
|
|
1162
|
-
// TEST107: Test extensions field propagates from media
|
|
1162
|
+
// TEST107: Test extensions field propagates from media def def to resolved
|
|
1163
1163
|
function test107_extensionsPropagation() {
|
|
1164
|
-
const
|
|
1164
|
+
const mediaDefs = [
|
|
1165
1165
|
{
|
|
1166
1166
|
urn: 'media:pdf',
|
|
1167
1167
|
media_type: 'application/pdf',
|
|
@@ -1169,7 +1169,7 @@ function test107_extensionsPropagation() {
|
|
|
1169
1169
|
extensions: ['pdf']
|
|
1170
1170
|
}
|
|
1171
1171
|
];
|
|
1172
|
-
const resolved = resolveMediaUrn('media:pdf',
|
|
1172
|
+
const resolved = resolveMediaUrn('media:pdf', mediaDefs);
|
|
1173
1173
|
assert(Array.isArray(resolved.extensions), 'Extensions should be an array');
|
|
1174
1174
|
assertEqual(resolved.extensions.length, 1, 'Should have one extension');
|
|
1175
1175
|
assertEqual(resolved.extensions[0], 'pdf', 'Should have pdf extension');
|
|
@@ -1177,15 +1177,15 @@ function test107_extensionsPropagation() {
|
|
|
1177
1177
|
|
|
1178
1178
|
// TEST108: Test creating new cap with URN, title, and command verifies correct initialization
|
|
1179
1179
|
function test108_extensionsSerialization() {
|
|
1180
|
-
// Test that
|
|
1181
|
-
const spec = new
|
|
1180
|
+
// Test that MediaDef can hold extensions correctly
|
|
1181
|
+
const spec = new MediaDef('application/pdf', null, null, 'PDF', null, 'media:pdf', null, null, ['pdf']);
|
|
1182
1182
|
assert(Array.isArray(spec.extensions), 'Extensions should be array');
|
|
1183
1183
|
assertEqual(spec.extensions[0], 'pdf', 'Should have pdf extension');
|
|
1184
1184
|
}
|
|
1185
1185
|
|
|
1186
1186
|
// TEST109: Test creating cap with metadata initializes and retrieves metadata correctly
|
|
1187
1187
|
function test109_extensionsWithMetadataAndValidation() {
|
|
1188
|
-
const
|
|
1188
|
+
const mediaDefs = [
|
|
1189
1189
|
{
|
|
1190
1190
|
urn: 'media:custom-output',
|
|
1191
1191
|
media_type: 'application/json',
|
|
@@ -1195,7 +1195,7 @@ function test109_extensionsWithMetadataAndValidation() {
|
|
|
1195
1195
|
extensions: ['json']
|
|
1196
1196
|
}
|
|
1197
1197
|
];
|
|
1198
|
-
const resolved = resolveMediaUrn('media:custom-output',
|
|
1198
|
+
const resolved = resolveMediaUrn('media:custom-output', mediaDefs);
|
|
1199
1199
|
assert(resolved.validation !== null, 'Should have validation');
|
|
1200
1200
|
assert(resolved.metadata !== null, 'Should have metadata');
|
|
1201
1201
|
assert(Array.isArray(resolved.extensions), 'Should have extensions');
|
|
@@ -1204,7 +1204,7 @@ function test109_extensionsWithMetadataAndValidation() {
|
|
|
1204
1204
|
|
|
1205
1205
|
// TEST110: Test cap matching with subset semantics for request fulfillment
|
|
1206
1206
|
function test110_multipleExtensions() {
|
|
1207
|
-
const
|
|
1207
|
+
const mediaDefs = [
|
|
1208
1208
|
{
|
|
1209
1209
|
urn: 'media:image;jpeg',
|
|
1210
1210
|
media_type: 'image/jpeg',
|
|
@@ -1212,7 +1212,7 @@ function test110_multipleExtensions() {
|
|
|
1212
1212
|
extensions: ['jpg', 'jpeg']
|
|
1213
1213
|
}
|
|
1214
1214
|
];
|
|
1215
|
-
const resolved = resolveMediaUrn('media:image;jpeg',
|
|
1215
|
+
const resolved = resolveMediaUrn('media:image;jpeg', mediaDefs);
|
|
1216
1216
|
assertEqual(resolved.extensions.length, 2, 'Should have two extensions');
|
|
1217
1217
|
assertEqual(resolved.extensions[0], 'jpg', 'First extension should be jpg');
|
|
1218
1218
|
assertEqual(resolved.extensions[1], 'jpeg', 'Second extension should be jpeg');
|
|
@@ -1585,7 +1585,7 @@ function test306_availabilityAndPathOutputDistinct() {
|
|
|
1585
1585
|
assert(!matchResult, 'availability must not conform to path');
|
|
1586
1586
|
}
|
|
1587
1587
|
|
|
1588
|
-
// TEST307: Test model_availability_urn builds valid cap URN with correct op and media
|
|
1588
|
+
// TEST307: Test model_availability_urn builds valid cap URN with correct op and media defs
|
|
1589
1589
|
function test307_modelAvailabilityUrn() {
|
|
1590
1590
|
const urn = modelAvailabilityUrn();
|
|
1591
1591
|
assert(urn.hasMarkerTag('model-availability'), 'Must have model-availability marker');
|
|
@@ -1597,7 +1597,7 @@ function test307_modelAvailabilityUrn() {
|
|
|
1597
1597
|
assert(outSpec.conformsTo(expectedOut), 'output must conform to MEDIA_AVAILABILITY_OUTPUT');
|
|
1598
1598
|
}
|
|
1599
1599
|
|
|
1600
|
-
// TEST308: Test model_path_urn builds valid cap URN with correct op and media
|
|
1600
|
+
// TEST308: Test model_path_urn builds valid cap URN with correct op and media defs
|
|
1601
1601
|
function test308_modelPathUrn() {
|
|
1602
1602
|
const urn = modelPathUrn();
|
|
1603
1603
|
assert(urn.hasMarkerTag('model-path'), 'Must have model-path marker');
|
|
@@ -1661,12 +1661,12 @@ function test312_allUrnBuildersProduceValidUrns() {
|
|
|
1661
1661
|
// but are important for capdag-js correctness.
|
|
1662
1662
|
|
|
1663
1663
|
function testJS_buildExtensionIndex() {
|
|
1664
|
-
const
|
|
1664
|
+
const mediaDefs = [
|
|
1665
1665
|
{ urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
|
|
1666
1666
|
{ urn: 'media:image;jpeg', media_type: 'image/jpeg', extensions: ['jpg', 'jpeg'] },
|
|
1667
1667
|
{ urn: 'media:json;textable', media_type: 'application/json', extensions: ['json'] }
|
|
1668
1668
|
];
|
|
1669
|
-
const index = buildExtensionIndex(
|
|
1669
|
+
const index = buildExtensionIndex(mediaDefs);
|
|
1670
1670
|
assert(index instanceof Map, 'Should return a Map');
|
|
1671
1671
|
assertEqual(index.size, 4, 'Should have 4 extensions');
|
|
1672
1672
|
assert(index.has('pdf'), 'Should have pdf');
|
|
@@ -1677,51 +1677,51 @@ function testJS_buildExtensionIndex() {
|
|
|
1677
1677
|
}
|
|
1678
1678
|
|
|
1679
1679
|
function testJS_mediaUrnsForExtension() {
|
|
1680
|
-
const
|
|
1680
|
+
const mediaDefs = [
|
|
1681
1681
|
{ urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
|
|
1682
1682
|
{ urn: 'media:json;textable;record', media_type: 'application/json', extensions: ['json'] },
|
|
1683
1683
|
{ urn: 'media:json;textable;list', media_type: 'application/json', extensions: ['json'] }
|
|
1684
1684
|
];
|
|
1685
1685
|
|
|
1686
|
-
const pdfUrns = mediaUrnsForExtension('pdf',
|
|
1686
|
+
const pdfUrns = mediaUrnsForExtension('pdf', mediaDefs);
|
|
1687
1687
|
assertEqual(pdfUrns.length, 1, 'Should find 1 URN for pdf');
|
|
1688
1688
|
|
|
1689
1689
|
// Case insensitivity
|
|
1690
|
-
const pdfUrnsUpper = mediaUrnsForExtension('PDF',
|
|
1690
|
+
const pdfUrnsUpper = mediaUrnsForExtension('PDF', mediaDefs);
|
|
1691
1691
|
assertEqual(pdfUrnsUpper.length, 1, 'Should find URN with uppercase extension');
|
|
1692
1692
|
|
|
1693
1693
|
// Multiple URNs for same extension
|
|
1694
|
-
const jsonUrns = mediaUrnsForExtension('json',
|
|
1694
|
+
const jsonUrns = mediaUrnsForExtension('json', mediaDefs);
|
|
1695
1695
|
assertEqual(jsonUrns.length, 2, 'Should find 2 URNs for json');
|
|
1696
1696
|
|
|
1697
1697
|
// Unknown extension throws
|
|
1698
1698
|
let thrownError = null;
|
|
1699
1699
|
try {
|
|
1700
|
-
mediaUrnsForExtension('unknown',
|
|
1700
|
+
mediaUrnsForExtension('unknown', mediaDefs);
|
|
1701
1701
|
} catch (e) {
|
|
1702
1702
|
thrownError = e;
|
|
1703
1703
|
}
|
|
1704
|
-
assert(thrownError instanceof
|
|
1704
|
+
assert(thrownError instanceof MediaDefError, 'Should throw MediaDefError for unknown ext');
|
|
1705
1705
|
}
|
|
1706
1706
|
|
|
1707
1707
|
function testJS_getExtensionMappings() {
|
|
1708
|
-
const
|
|
1708
|
+
const mediaDefs = [
|
|
1709
1709
|
{ urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
|
|
1710
1710
|
{ urn: 'media:image;jpeg', media_type: 'image/jpeg', extensions: ['jpg', 'jpeg'] }
|
|
1711
1711
|
];
|
|
1712
|
-
const mappings = getExtensionMappings(
|
|
1712
|
+
const mappings = getExtensionMappings(mediaDefs);
|
|
1713
1713
|
assert(Array.isArray(mappings), 'Should return an array');
|
|
1714
1714
|
assertEqual(mappings.length, 3, 'Should have 3 mappings');
|
|
1715
1715
|
}
|
|
1716
1716
|
|
|
1717
1717
|
function testJS_resolveMediaUrnFromSpecs() {
|
|
1718
|
-
const
|
|
1718
|
+
const mediaDefs = [
|
|
1719
1719
|
{ urn: MEDIA_STRING, media_type: 'text/plain', title: 'String', profile_uri: 'https://capdag.com/schema/str' },
|
|
1720
1720
|
{ urn: 'media:custom', media_type: 'application/json', title: 'Custom Output', schema: { type: 'object' } }
|
|
1721
1721
|
];
|
|
1722
|
-
const strSpec = resolveMediaUrn(MEDIA_STRING,
|
|
1722
|
+
const strSpec = resolveMediaUrn(MEDIA_STRING, mediaDefs);
|
|
1723
1723
|
assertEqual(strSpec.contentType, 'text/plain', 'Should resolve string spec');
|
|
1724
|
-
const outputSpec = resolveMediaUrn('media:custom',
|
|
1724
|
+
const outputSpec = resolveMediaUrn('media:custom', mediaDefs);
|
|
1725
1725
|
assertEqual(outputSpec.contentType, 'application/json', 'Should resolve custom spec');
|
|
1726
1726
|
assert(outputSpec.schema !== null, 'Should have schema');
|
|
1727
1727
|
}
|
|
@@ -1737,7 +1737,7 @@ function testJS_capJSONSerialization() {
|
|
|
1737
1737
|
|
|
1738
1738
|
const json = cap.toJSON();
|
|
1739
1739
|
assertEqual(typeof json.urn, 'string', 'URN should be string');
|
|
1740
|
-
assert(json.
|
|
1740
|
+
assert(json.media_defs === undefined, 'Cap JSON must not contain media_defs (registry-resolved)');
|
|
1741
1741
|
|
|
1742
1742
|
const restored = Cap.fromJSON(json);
|
|
1743
1743
|
assertEqual(restored.urn.getInSpec(), MEDIA_VOID, 'Should restore inSpec');
|
|
@@ -1790,13 +1790,13 @@ function testJS_capDocumentationOmittedWhenNull() {
|
|
|
1790
1790
|
assertEqual(cap.getDocumentation(), null, 'Empty string must collapse to null');
|
|
1791
1791
|
}
|
|
1792
1792
|
|
|
1793
|
-
// Documentation propagates from a
|
|
1794
|
-
// resolveMediaUrn into the resolved
|
|
1793
|
+
// Documentation propagates from a mediaDefs definition through
|
|
1794
|
+
// resolveMediaUrn into the resolved MediaDef. Mirrors TEST924 on the Rust
|
|
1795
1795
|
// side. This is the path every UI consumer uses, so a break here makes the
|
|
1796
1796
|
// new field invisible everywhere downstream.
|
|
1797
|
-
function
|
|
1797
|
+
function testJS_mediaDefDocumentationPropagatesThroughResolve() {
|
|
1798
1798
|
const body = '## Markdown body\n\nWith `code` and a [link](https://example.com).';
|
|
1799
|
-
const
|
|
1799
|
+
const mediaDefs = [
|
|
1800
1800
|
{
|
|
1801
1801
|
urn: 'media:doc-test;textable',
|
|
1802
1802
|
media_type: 'text/plain',
|
|
@@ -1806,8 +1806,8 @@ function testJS_mediaSpecDocumentationPropagatesThroughResolve() {
|
|
|
1806
1806
|
}
|
|
1807
1807
|
];
|
|
1808
1808
|
|
|
1809
|
-
const resolved = resolveMediaUrn('media:doc-test;textable',
|
|
1810
|
-
assertEqual(resolved.documentation, body, 'documentation must propagate into
|
|
1809
|
+
const resolved = resolveMediaUrn('media:doc-test;textable', mediaDefs);
|
|
1810
|
+
assertEqual(resolved.documentation, body, 'documentation must propagate into MediaDef');
|
|
1811
1811
|
// The short description must remain distinct from the long markdown
|
|
1812
1812
|
// body — they are different fields with different semantics.
|
|
1813
1813
|
assertEqual(resolved.description, 'short desc', 'description must remain distinct from documentation');
|
|
@@ -1837,14 +1837,14 @@ function testJS_stdinSourceNullData() {
|
|
|
1837
1837
|
assertEqual(source.data, null, 'Data should be null');
|
|
1838
1838
|
}
|
|
1839
1839
|
|
|
1840
|
-
function
|
|
1841
|
-
const spec1 = new
|
|
1840
|
+
function testJS_mediaDefConstruction() {
|
|
1841
|
+
const spec1 = new MediaDef('text/plain', 'https://capdag.com/schema/str', null, 'String', null, 'media:string');
|
|
1842
1842
|
assertEqual(spec1.contentType, 'text/plain', 'Should have content type');
|
|
1843
1843
|
assertEqual(spec1.profile, 'https://capdag.com/schema/str', 'Should have profile');
|
|
1844
1844
|
assertEqual(spec1.title, 'String', 'Should have title');
|
|
1845
1845
|
assertEqual(spec1.mediaUrn, 'media:string', 'Should have mediaUrn');
|
|
1846
1846
|
|
|
1847
|
-
const spec2 = new
|
|
1847
|
+
const spec2 = new MediaDef('application/octet-stream', null, null, 'Binary', null, 'media:binary');
|
|
1848
1848
|
assertEqual(spec2.profile, null, 'Should have null profile');
|
|
1849
1849
|
}
|
|
1850
1850
|
|
|
@@ -2562,9 +2562,9 @@ function test1303_withoutTag() {
|
|
|
2562
2562
|
const removed2 = cap.withoutTag('EXT');
|
|
2563
2563
|
assertEqual(removed2.getTag('ext'), undefined, 'withoutTag should be case-insensitive');
|
|
2564
2564
|
|
|
2565
|
-
assertThrows(() => cap.withoutTag('in'), 'withoutTag must reject in');
|
|
2566
|
-
assertThrows(() => cap.withoutTag('out'), 'withoutTag must reject out');
|
|
2567
|
-
assertThrows(() => cap.withoutTag('effect'), 'withoutTag must reject effect');
|
|
2565
|
+
assertThrows(() => cap.withoutTag('in'), ErrorCodes.INVALID_TAG_FORMAT, 'withoutTag must reject in');
|
|
2566
|
+
assertThrows(() => cap.withoutTag('out'), ErrorCodes.INVALID_TAG_FORMAT, 'withoutTag must reject out');
|
|
2567
|
+
assertThrows(() => cap.withoutTag('effect'), ErrorCodes.INVALID_TAG_FORMAT, 'withoutTag must reject effect');
|
|
2568
2568
|
|
|
2569
2569
|
// Removing non-existent tag is no-op
|
|
2570
2570
|
const same3 = cap.withoutTag('nonexistent');
|
|
@@ -2587,11 +2587,12 @@ function test1304_withInOutSpec() {
|
|
|
2587
2587
|
// Chain both
|
|
2588
2588
|
const changedBoth = cap.withInSpec('media:pdf').withOutSpec(MEDIA_TXT);
|
|
2589
2589
|
assertEqual(changedBoth.getInSpec(), 'media:pdf', 'Chain should set inSpec');
|
|
2590
|
-
assertEqual(changedBoth.getOutSpec(),
|
|
2590
|
+
assertEqual(changedBoth.getOutSpec(), 'media:textable;txt', 'Chain should set outSpec');
|
|
2591
2591
|
|
|
2592
2592
|
const identity = CapUrn.fromString('cap:effect=none');
|
|
2593
2593
|
assertThrows(
|
|
2594
2594
|
() => identity.withOutSpec('media:pdf'),
|
|
2595
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2595
2596
|
'withOutSpec must revalidate admissibility'
|
|
2596
2597
|
);
|
|
2597
2598
|
}
|
|
@@ -2646,23 +2647,26 @@ function test1306_areCompatible() {
|
|
|
2646
2647
|
// TEST1307: with_tag rejects structural keys
|
|
2647
2648
|
function test1307_withTagRejectsStructuralKeys() {
|
|
2648
2649
|
const cap = CapUrn.fromString('cap:in="media:void";test;out="media:void"');
|
|
2649
|
-
assertThrows(() => cap.withTag('in', 'media:'), 'withTag must reject in');
|
|
2650
|
-
assertThrows(() => cap.withTag('out', 'media:'), 'withTag must reject out');
|
|
2651
|
-
assertThrows(() => cap.withTag('effect', 'none'), 'withTag must reject effect');
|
|
2650
|
+
assertThrows(() => cap.withTag('in', 'media:'), ErrorCodes.INVALID_TAG_FORMAT, 'withTag must reject in');
|
|
2651
|
+
assertThrows(() => cap.withTag('out', 'media:'), ErrorCodes.INVALID_TAG_FORMAT, 'withTag must reject out');
|
|
2652
|
+
assertThrows(() => cap.withTag('effect', 'none'), ErrorCodes.INVALID_TAG_FORMAT, 'withTag must reject effect');
|
|
2652
2653
|
}
|
|
2653
2654
|
|
|
2654
2655
|
// TEST1308: builder rejects structural keys on tag/marker
|
|
2655
2656
|
function test1308_builderRejectsStructuralKeys() {
|
|
2656
2657
|
assertThrows(
|
|
2657
2658
|
() => new CapUrnBuilder().tag('in', 'media:void'),
|
|
2659
|
+
ErrorCodes.INVALID_TAG_FORMAT,
|
|
2658
2660
|
'builder.tag must reject structural in'
|
|
2659
2661
|
);
|
|
2660
2662
|
assertThrows(
|
|
2661
2663
|
() => new CapUrnBuilder().marker('effect'),
|
|
2664
|
+
ErrorCodes.INVALID_TAG_FORMAT,
|
|
2662
2665
|
'builder.marker must reject structural effect'
|
|
2663
2666
|
);
|
|
2664
2667
|
assertThrows(
|
|
2665
2668
|
() => new CapUrnBuilder().inSpec('media:void').outSpec('media:record').tag('123', 'value').build(),
|
|
2669
|
+
ErrorCodes.NUMERIC_KEY,
|
|
2666
2670
|
'builder.build must reject invalid non-structural tags'
|
|
2667
2671
|
);
|
|
2668
2672
|
}
|
|
@@ -3910,7 +3914,7 @@ function testMachine_capRegistryEntry_construction() {
|
|
|
3910
3914
|
cap_description: 'Extracts text from PDF',
|
|
3911
3915
|
args: [{ media_urn: 'media:pdf', required: true }],
|
|
3912
3916
|
output: { media_urn: 'media:txt;textable', output_description: 'Extracted text' },
|
|
3913
|
-
|
|
3917
|
+
media_defs: [],
|
|
3914
3918
|
urn_tags: { op: 'extract' },
|
|
3915
3919
|
in_spec: 'media:pdf',
|
|
3916
3920
|
out_spec: 'media:txt;textable',
|
|
@@ -4181,19 +4185,19 @@ function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
|
|
|
4181
4185
|
};
|
|
4182
4186
|
}
|
|
4183
4187
|
|
|
4184
|
-
function makeForEachStep(
|
|
4188
|
+
function makeForEachStep(mediaDef) {
|
|
4185
4189
|
return {
|
|
4186
|
-
step_type: { ForEach: {
|
|
4187
|
-
from_spec:
|
|
4188
|
-
to_spec:
|
|
4190
|
+
step_type: { ForEach: { media_def: mediaDef } },
|
|
4191
|
+
from_spec: mediaDef,
|
|
4192
|
+
to_spec: mediaDef,
|
|
4189
4193
|
};
|
|
4190
4194
|
}
|
|
4191
4195
|
|
|
4192
|
-
function makeCollectStep(
|
|
4196
|
+
function makeCollectStep(mediaDef) {
|
|
4193
4197
|
return {
|
|
4194
|
-
step_type: { Collect: {
|
|
4195
|
-
from_spec:
|
|
4196
|
-
to_spec:
|
|
4198
|
+
step_type: { Collect: { media_def: mediaDef } },
|
|
4199
|
+
from_spec: mediaDef,
|
|
4200
|
+
to_spec: mediaDef,
|
|
4197
4201
|
};
|
|
4198
4202
|
}
|
|
4199
4203
|
|
|
@@ -4293,8 +4297,8 @@ function testRenderer_buildStrandGraphData_singleCapPlain() {
|
|
|
4293
4297
|
// (two edges, three nodes). No cardinality marker in the cap label
|
|
4294
4298
|
// because input_is_sequence == output_is_sequence == false.
|
|
4295
4299
|
const payload = withMediaDisplayNames({
|
|
4296
|
-
|
|
4297
|
-
|
|
4300
|
+
source_media_urn: 'media:a',
|
|
4301
|
+
target_media_urn: 'media:b',
|
|
4298
4302
|
steps: [
|
|
4299
4303
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4300
4304
|
],
|
|
@@ -4318,8 +4322,8 @@ function testRenderer_buildStrandGraphData_sequenceShowsCardinality() {
|
|
|
4318
4322
|
// A cap with input_is_sequence=true MUST emit "(n→1)" on its edge
|
|
4319
4323
|
// label.
|
|
4320
4324
|
const payload = withMediaDisplayNames({
|
|
4321
|
-
|
|
4322
|
-
|
|
4325
|
+
source_media_urn: 'media:a;list',
|
|
4326
|
+
target_media_urn: 'media:b',
|
|
4323
4327
|
steps: [
|
|
4324
4328
|
makeCapStep('cap:in="media:a;list";x;out="media:b"', 'x', 'media:a;list', 'media:b', true, false),
|
|
4325
4329
|
],
|
|
@@ -4348,8 +4352,8 @@ function testRenderer_buildStrandGraphData_foreachCollectSpan() {
|
|
|
4348
4352
|
// labels on cap edges — they're distinct processing units in the
|
|
4349
4353
|
// plan. This mirrors capdag's plan_builder.rs exactly.
|
|
4350
4354
|
const payload = withMediaDisplayNames({
|
|
4351
|
-
|
|
4352
|
-
|
|
4355
|
+
source_media_urn: 'media:pdf;list',
|
|
4356
|
+
target_media_urn: 'media:txt;list',
|
|
4353
4357
|
steps: [
|
|
4354
4358
|
makeForEachStep('media:pdf;list'),
|
|
4355
4359
|
makeCapStep('cap:in="media:pdf";extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
|
|
@@ -4390,8 +4394,8 @@ function testRenderer_buildStrandGraphData_standaloneCollect() {
|
|
|
4390
4394
|
// builder creates a Collect node consuming prev directly — plain
|
|
4391
4395
|
// direct edge, no iteration/collection semantics.
|
|
4392
4396
|
const payload = withMediaDisplayNames({
|
|
4393
|
-
|
|
4394
|
-
|
|
4397
|
+
source_media_urn: 'media:a',
|
|
4398
|
+
target_media_urn: 'media:b;list',
|
|
4395
4399
|
steps: [
|
|
4396
4400
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4397
4401
|
makeCollectStep('media:b'),
|
|
@@ -4418,8 +4422,8 @@ function testRenderer_buildStrandGraphData_unclosedForEachBody() {
|
|
|
4418
4422
|
// connecting Cap_a to Cap_b via iteration, with prev becoming the
|
|
4419
4423
|
// body exit (Cap_b).
|
|
4420
4424
|
const payload = withMediaDisplayNames({
|
|
4421
|
-
|
|
4422
|
-
|
|
4425
|
+
source_media_urn: 'media:a',
|
|
4426
|
+
target_media_urn: 'media:c',
|
|
4423
4427
|
steps: [
|
|
4424
4428
|
makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4425
4429
|
makeForEachStep('media:b'),
|
|
@@ -4455,8 +4459,8 @@ function testRenderer_buildStrandGraphData_nestedForEachThrows() {
|
|
|
4455
4459
|
// must throw the same error to surface the issue rather than
|
|
4456
4460
|
// render a malformed graph.
|
|
4457
4461
|
const payload = withMediaDisplayNames({
|
|
4458
|
-
|
|
4459
|
-
|
|
4462
|
+
source_media_urn: 'media:a;list',
|
|
4463
|
+
target_media_urn: 'media:a',
|
|
4460
4464
|
steps: [
|
|
4461
4465
|
makeForEachStep('media:a;list'),
|
|
4462
4466
|
makeForEachStep('media:a'),
|
|
@@ -4492,8 +4496,8 @@ function testRenderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
|
|
|
4492
4496
|
// with the entry edge labeled "extract" and an unlabeled
|
|
4493
4497
|
// connector bridge to the output.
|
|
4494
4498
|
const payload = withMediaDisplayNames({
|
|
4495
|
-
|
|
4496
|
-
|
|
4499
|
+
source_media_urn: 'media:pdf;list',
|
|
4500
|
+
target_media_urn: 'media:txt;list',
|
|
4497
4501
|
steps: [
|
|
4498
4502
|
makeForEachStep('media:pdf;list'),
|
|
4499
4503
|
makeCapStep('cap:in="media:pdf";extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
|
|
@@ -4532,7 +4536,7 @@ function testRenderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
|
|
|
4532
4536
|
function testRenderer_collapseStrand_unclosedForEachBodyCollapses() {
|
|
4533
4537
|
// [Cap_a(1→1), ForEach, Cap_b(1→1)] with no Collect,
|
|
4534
4538
|
// source=media:a, target=media:c. Cap_b's to_spec is media:c
|
|
4535
|
-
// which is equivalent to
|
|
4539
|
+
// which is equivalent to target_media_urn, so the output node is
|
|
4536
4540
|
// merged into step_2.
|
|
4537
4541
|
//
|
|
4538
4542
|
// Since both caps are 1→1, neither carries a cardinality
|
|
@@ -4541,8 +4545,8 @@ function testRenderer_collapseStrand_unclosedForEachBodyCollapses() {
|
|
|
4541
4545
|
//
|
|
4542
4546
|
// Final: 3 nodes (input_slot, step_0, step_2), 2 edges.
|
|
4543
4547
|
const payload = withMediaDisplayNames({
|
|
4544
|
-
|
|
4545
|
-
|
|
4548
|
+
source_media_urn: 'media:a',
|
|
4549
|
+
target_media_urn: 'media:c',
|
|
4546
4550
|
steps: [
|
|
4547
4551
|
makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
|
|
4548
4552
|
makeForEachStep('media:b'),
|
|
@@ -4598,8 +4602,8 @@ function testRenderer_collapseStrand_standaloneCollectCollapses() {
|
|
|
4598
4602
|
//
|
|
4599
4603
|
// Final: 3 nodes (input_slot, step_0, output), 2 edges.
|
|
4600
4604
|
const payload = withMediaDisplayNames({
|
|
4601
|
-
|
|
4602
|
-
|
|
4605
|
+
source_media_urn: 'media:a',
|
|
4606
|
+
target_media_urn: 'media:b;list',
|
|
4603
4607
|
steps: [
|
|
4604
4608
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4605
4609
|
makeCollectStep('media:b'),
|
|
@@ -4646,8 +4650,8 @@ function testRenderer_collapseStrand_sequenceProducingCapBeforeForeach() {
|
|
|
4646
4650
|
// No separate output node because step_2's to_spec equals the
|
|
4647
4651
|
// strand target.
|
|
4648
4652
|
const payload = withMediaDisplayNames({
|
|
4649
|
-
|
|
4650
|
-
|
|
4653
|
+
source_media_urn: 'media:pdf',
|
|
4654
|
+
target_media_urn: 'media:decision',
|
|
4651
4655
|
steps: [
|
|
4652
4656
|
makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
|
|
4653
4657
|
makeForEachStep('media:page'),
|
|
@@ -4694,15 +4698,15 @@ function testRenderer_collapseStrand_sequenceProducingCapBeforeForeach() {
|
|
|
4694
4698
|
|
|
4695
4699
|
function testRenderer_collapseStrand_plainCapMergesTrailingOutput() {
|
|
4696
4700
|
// A strand with a single plain 1→1 cap whose to_spec equals
|
|
4697
|
-
//
|
|
4701
|
+
// target_media_urn. The plan-builder topology produces:
|
|
4698
4702
|
// input_slot → step_0 (cap) → output
|
|
4699
4703
|
// The collapse pass merges the trailing output edge because
|
|
4700
4704
|
// step_0 and output represent the same URN (media:b).
|
|
4701
4705
|
//
|
|
4702
4706
|
// Final: 2 nodes (input_slot, step_0), 1 edge.
|
|
4703
4707
|
const payload = withMediaDisplayNames({
|
|
4704
|
-
|
|
4705
|
-
|
|
4708
|
+
source_media_urn: 'media:a',
|
|
4709
|
+
target_media_urn: 'media:b',
|
|
4706
4710
|
steps: [
|
|
4707
4711
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4708
4712
|
],
|
|
@@ -4730,11 +4734,11 @@ function testRenderer_collapseStrand_plainCapMergesTrailingOutput() {
|
|
|
4730
4734
|
|
|
4731
4735
|
function testRenderer_collapseStrand_plainCapDistinctTargetNoMerge() {
|
|
4732
4736
|
// A strand with a single plain cap whose to_spec is NOT
|
|
4733
|
-
// equivalent to
|
|
4737
|
+
// equivalent to target_media_urn. The output node must be retained
|
|
4734
4738
|
// and the trailing connector edge preserved.
|
|
4735
4739
|
const payload = withMediaDisplayNames({
|
|
4736
|
-
|
|
4737
|
-
|
|
4740
|
+
source_media_urn: 'media:a',
|
|
4741
|
+
target_media_urn: 'media:b;list',
|
|
4738
4742
|
steps: [
|
|
4739
4743
|
makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
|
|
4740
4744
|
],
|
|
@@ -4754,15 +4758,15 @@ function testRenderer_collapseStrand_plainCapDistinctTargetNoMerge() {
|
|
|
4754
4758
|
'step_0 retained');
|
|
4755
4759
|
}
|
|
4756
4760
|
|
|
4757
|
-
function
|
|
4761
|
+
function testRenderer_validateStrandPayload_missingSourceMediaUrn() {
|
|
4758
4762
|
let threw = false;
|
|
4759
4763
|
try {
|
|
4760
|
-
rendererValidateStrandPayload({
|
|
4764
|
+
rendererValidateStrandPayload({ target_media_urn: 'media:b', steps: [] });
|
|
4761
4765
|
} catch (e) {
|
|
4762
4766
|
threw = true;
|
|
4763
|
-
assert(e.message.includes('
|
|
4767
|
+
assert(e.message.includes('source_media_urn'), 'error must name source_media_urn');
|
|
4764
4768
|
}
|
|
4765
|
-
assert(threw, 'missing
|
|
4769
|
+
assert(threw, 'missing source_media_urn must throw');
|
|
4766
4770
|
}
|
|
4767
4771
|
|
|
4768
4772
|
// ---------------- run builder ----------------
|
|
@@ -4789,8 +4793,8 @@ function testRenderer_buildRunGraphData_pagesSuccessesAndFailures() {
|
|
|
4789
4793
|
// Show-more nodes: one for 3 hidden successes, one for 2 hidden
|
|
4790
4794
|
// failures.
|
|
4791
4795
|
const strand = {
|
|
4792
|
-
|
|
4793
|
-
|
|
4796
|
+
source_media_urn: 'media:pdf;list',
|
|
4797
|
+
target_media_urn: 'media:txt',
|
|
4794
4798
|
steps: [
|
|
4795
4799
|
makeForEachStep('media:pdf;list'),
|
|
4796
4800
|
makeCapStep('cap:in="media:pdf";a;out="media:image;png"', 'a', 'media:pdf', 'media:image;png', false, false),
|
|
@@ -4855,8 +4859,8 @@ function testRenderer_buildRunGraphData_failureWithoutFailedCapRendersFullTrace(
|
|
|
4855
4859
|
// Strand [ForEach, Cap, Collect] → body has 1 cap. Each body
|
|
4856
4860
|
// replica emits 1 entry node + 1 body cap node = 2 nodes.
|
|
4857
4861
|
const strand = {
|
|
4858
|
-
|
|
4859
|
-
|
|
4862
|
+
source_media_urn: 'media:pdf;list',
|
|
4863
|
+
target_media_urn: 'media:txt',
|
|
4860
4864
|
steps: [
|
|
4861
4865
|
makeForEachStep('media:pdf;list'),
|
|
4862
4866
|
makeCapStep('cap:in="media:pdf";a;out="media:txt"', 'a', 'media:pdf', 'media:txt', false, false),
|
|
@@ -4891,8 +4895,8 @@ function testRenderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap() {
|
|
|
4891
4895
|
// failed_cap and the step's cap_urn differ only in tag order — they
|
|
4892
4896
|
// should still match, proving URNs are not treated as strings.
|
|
4893
4897
|
const strand = {
|
|
4894
|
-
|
|
4895
|
-
|
|
4898
|
+
source_media_urn: 'media:a',
|
|
4899
|
+
target_media_urn: 'media:c',
|
|
4896
4900
|
steps: [
|
|
4897
4901
|
makeForEachStep('media:a;list'),
|
|
4898
4902
|
// Canonical form places tags alphabetically: op after in/out.
|
|
@@ -4952,13 +4956,13 @@ function testRenderer_buildRunGraphData_backboneHasNoForeachNode() {
|
|
|
4952
4956
|
// concepts don't leak into the view as boxed nodes.
|
|
4953
4957
|
//
|
|
4954
4958
|
// User scenario: [Disbind (1→n), ForEach, make_decision] where
|
|
4955
|
-
//
|
|
4959
|
+
// target_media_urn equals the last cap's to_spec, so the backbone
|
|
4956
4960
|
// collapses to 3 nodes: input_slot, step_0 (Text Page),
|
|
4957
4961
|
// step_2 (Decision, merged target). No separate `for each` or
|
|
4958
4962
|
// `collect` boxes.
|
|
4959
4963
|
const strand = {
|
|
4960
|
-
|
|
4961
|
-
|
|
4964
|
+
source_media_urn: 'media:pdf',
|
|
4965
|
+
target_media_urn: 'media:decision',
|
|
4962
4966
|
steps: [
|
|
4963
4967
|
makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
|
|
4964
4968
|
makeForEachStep('media:page'),
|
|
@@ -4989,7 +4993,7 @@ function testRenderer_buildRunGraphData_backboneHasNoForeachNode() {
|
|
|
4989
4993
|
// that runs from the pre-foreach node to the body cap. It must
|
|
4990
4994
|
// survive collapse so the target stays reachable even with zero
|
|
4991
4995
|
// successful bodies.
|
|
4992
|
-
const backboneCapEdges = built.strandBuilt.edges.filter(e => e.edgeClass
|
|
4996
|
+
const backboneCapEdges = built.strandBuilt.edges.filter(e => e.edgeClass.indexOf('strand-cap-edge') >= 0);
|
|
4993
4997
|
assert(backboneCapEdges.some(e => e.source === 'step_0' && e.target === 'step_2'),
|
|
4994
4998
|
'foreach-entry backbone edge step_0 → step_2 must be present for fallback connectivity');
|
|
4995
4999
|
|
|
@@ -5005,8 +5009,8 @@ function testRenderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
|
|
|
5005
5009
|
// doesn't see a stale "Decision" placeholder alongside their
|
|
5006
5010
|
// failed replicas.
|
|
5007
5011
|
const strand = {
|
|
5008
|
-
|
|
5009
|
-
|
|
5012
|
+
source_media_urn: 'media:pdf',
|
|
5013
|
+
target_media_urn: 'media:decision',
|
|
5010
5014
|
steps: [
|
|
5011
5015
|
makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
|
|
5012
5016
|
makeForEachStep('media:page'),
|
|
@@ -5075,8 +5079,8 @@ function testRenderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
|
|
|
5075
5079
|
// → body_n_0 (per-body Decision)
|
|
5076
5080
|
// (no merge edge back into the backbone)
|
|
5077
5081
|
const strand = {
|
|
5078
|
-
|
|
5079
|
-
|
|
5082
|
+
source_media_urn: 'media:pdf',
|
|
5083
|
+
target_media_urn: 'media:decision',
|
|
5080
5084
|
steps: [
|
|
5081
5085
|
makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
|
|
5082
5086
|
makeForEachStep('media:page'),
|
|
@@ -5134,8 +5138,8 @@ function testRenderer_buildRunGraphData_closedForeachSuccessMergesAtCollectTarge
|
|
|
5134
5138
|
// Actually simpler: [ForEach, Cap_a, Collect] with source=list
|
|
5135
5139
|
// and target=list.
|
|
5136
5140
|
const strand = {
|
|
5137
|
-
|
|
5138
|
-
|
|
5141
|
+
source_media_urn: 'media:pdf;list',
|
|
5142
|
+
target_media_urn: 'media:txt;list',
|
|
5139
5143
|
steps: [
|
|
5140
5144
|
makeForEachStep('media:pdf;list'),
|
|
5141
5145
|
makeCapStep('cap:in="media:pdf";extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
|
|
@@ -5859,8 +5863,8 @@ function test1842_truthTableFullCrossProduct() {
|
|
|
5859
5863
|
for (let j = 0; j < forms.length; j++) {
|
|
5860
5864
|
const instForm = forms[i];
|
|
5861
5865
|
const pattForm = forms[j];
|
|
5862
|
-
const instStr = instForm === '' ? 'cap:' : 'cap:' + instForm;
|
|
5863
|
-
const pattStr = pattForm === '' ? 'cap:' : 'cap:' + pattForm;
|
|
5866
|
+
const instStr = instForm === '' ? 'cap:base' : 'cap:base;' + instForm;
|
|
5867
|
+
const pattStr = pattForm === '' ? 'cap:base' : 'cap:base;' + pattForm;
|
|
5864
5868
|
const inst = CapUrn.fromString(instStr);
|
|
5865
5869
|
const patt = CapUrn.fromString(pattStr);
|
|
5866
5870
|
const actual = patt.accepts(inst);
|
|
@@ -5981,7 +5985,7 @@ async function runTests() {
|
|
|
5981
5985
|
console.log(' SKIP TEST053: N/A for JS (Rust-only validation infrastructure)');
|
|
5982
5986
|
runTest('TEST054: xv5_inline_spec_redefinition_detected', test054_xv5InlineSpecRedefinitionDetected);
|
|
5983
5987
|
runTest('TEST055: xv5_new_inline_spec_allowed', test055_xv5NewInlineSpecAllowed);
|
|
5984
|
-
runTest('TEST056:
|
|
5988
|
+
runTest('TEST056: xv5_empty_media_defs_allowed', test056_xv5EmptyMediaDefsAllowed);
|
|
5985
5989
|
|
|
5986
5990
|
// media_urn.rs: TEST060-TEST078
|
|
5987
5991
|
console.log('\n--- media_urn.rs ---');
|
|
@@ -6004,10 +6008,10 @@ async function runTests() {
|
|
|
6004
6008
|
runTest('TEST077: serde_roundtrip (JSON.stringify)', test077_serdeRoundtrip);
|
|
6005
6009
|
runTest('TEST078: debug_matching_behavior', test078_debugMatchingBehavior);
|
|
6006
6010
|
|
|
6007
|
-
//
|
|
6008
|
-
console.log('\n---
|
|
6011
|
+
// media_def.rs: TEST088-TEST110
|
|
6012
|
+
console.log('\n--- media_def.rs ---');
|
|
6009
6013
|
console.log(' SKIP TEST088-090: N/A for JS (async registry, Rust-only)');
|
|
6010
|
-
runTest('TEST091:
|
|
6014
|
+
runTest('TEST091: resolve_custom_media_def', test091_resolveCustomMediaDef);
|
|
6011
6015
|
runTest('TEST092: resolve_custom_with_schema', test092_resolveCustomWithSchema);
|
|
6012
6016
|
runTest('TEST093: resolve_unresolvable_fails_hard', test093_resolveUnresolvableFailsHard);
|
|
6013
6017
|
console.log(' SKIP TEST094: N/A for JS (no registry concept)');
|
|
@@ -6076,10 +6080,10 @@ async function runTests() {
|
|
|
6076
6080
|
runTest('JS: cap_json_serialization', testJS_capJSONSerialization);
|
|
6077
6081
|
runTest('JS: cap_documentation_round_trip', testJS_capDocumentationRoundTrip);
|
|
6078
6082
|
runTest('JS: cap_documentation_omitted_when_null', testJS_capDocumentationOmittedWhenNull);
|
|
6079
|
-
runTest('JS:
|
|
6083
|
+
runTest('JS: media_def_documentation_propagates_through_resolve', testJS_mediaDefDocumentationPropagatesThroughResolve);
|
|
6080
6084
|
runTest('JS: stdin_source_kind_constants', testJS_stdinSourceKindConstants);
|
|
6081
6085
|
runTest('JS: stdin_source_null_data', testJS_stdinSourceNullData);
|
|
6082
|
-
runTest('JS:
|
|
6086
|
+
runTest('JS: media_def_construction', testJS_mediaDefConstruction);
|
|
6083
6087
|
|
|
6084
6088
|
// cartridge_repo: CartridgeRepoServer and CartridgeRepoClient tests
|
|
6085
6089
|
console.log('\n--- cartridge_repo ---');
|
|
@@ -6288,7 +6292,7 @@ async function runTests() {
|
|
|
6288
6292
|
runTest('RENDERER: collapseStrand_seqCapBeforeForeach', testRenderer_collapseStrand_sequenceProducingCapBeforeForeach);
|
|
6289
6293
|
runTest('RENDERER: collapseStrand_plainCapMergesOutput', testRenderer_collapseStrand_plainCapMergesTrailingOutput);
|
|
6290
6294
|
runTest('RENDERER: collapseStrand_plainCapDistinctTarget', testRenderer_collapseStrand_plainCapDistinctTargetNoMerge);
|
|
6291
|
-
runTest('RENDERER:
|
|
6295
|
+
runTest('RENDERER: validateStrand_missingSourceMediaUrn', testRenderer_validateStrandPayload_missingSourceMediaUrn);
|
|
6292
6296
|
|
|
6293
6297
|
console.log('\n--- cap-fab-renderer run builder ---');
|
|
6294
6298
|
runTest('RENDERER: validateBodyOutcome_negativeIndex', testRenderer_validateBodyOutcome_rejectsNegativeIndex);
|