capns 0.50.11396 → 0.58.11575
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 +33 -0
- package/capns.js +201 -31
- package/capns.test.js +154 -17
- package/package.json +2 -2
package/RULES.md
CHANGED
|
@@ -70,6 +70,39 @@ Examples:
|
|
|
70
70
|
| 11 | MISSING_OUT_SPEC | Cap URN missing required `out` tag |
|
|
71
71
|
| 12 | INVALID_MEDIA_URN | Direction spec value is not a valid Media URN |
|
|
72
72
|
|
|
73
|
+
## Validation Rules
|
|
74
|
+
|
|
75
|
+
### XV5: No Redefinition of Registry Media Specs
|
|
76
|
+
|
|
77
|
+
Inline media specs in a capability's `media_specs` table must not redefine media specs that already exist in the global registry or built-in specs.
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
const { validateNoMediaSpecRedefinitionSync, MEDIA_STRING } = require('capns');
|
|
81
|
+
|
|
82
|
+
// This will fail - MEDIA_STRING is a built-in spec
|
|
83
|
+
const mediaSpecs = {
|
|
84
|
+
[MEDIA_STRING]: { media_type: 'text/plain', title: 'My String' }
|
|
85
|
+
};
|
|
86
|
+
const result = validateNoMediaSpecRedefinitionSync(mediaSpecs);
|
|
87
|
+
// result: { valid: false, error: 'XV5: ...', redefines: ['media:textable;form=scalar'] }
|
|
88
|
+
|
|
89
|
+
// This is allowed - custom spec that doesn't exist
|
|
90
|
+
const customSpecs = {
|
|
91
|
+
'media:my-custom-type': { media_type: 'application/json', title: 'My Type' }
|
|
92
|
+
};
|
|
93
|
+
const customResult = validateNoMediaSpecRedefinitionSync(customSpecs);
|
|
94
|
+
// result: { valid: true }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For server-side validation with registry access, use the async version:
|
|
98
|
+
```javascript
|
|
99
|
+
const { validateNoMediaSpecRedefinition } = require('capns');
|
|
100
|
+
|
|
101
|
+
const result = await validateNoMediaSpecRedefinition(mediaSpecs, {
|
|
102
|
+
registryLookup: async (urn) => await mediaStore.get(urn) !== null
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
73
106
|
## Cross-Language Compatibility
|
|
74
107
|
|
|
75
108
|
This JavaScript implementation follows the same rules as:
|
package/capns.js
CHANGED
|
@@ -755,26 +755,28 @@ const MediaSpecErrorCodes = {
|
|
|
755
755
|
* Well-known built-in media URN constants
|
|
756
756
|
* These media URNs are implicitly available and do not need to be declared in mediaSpecs
|
|
757
757
|
*/
|
|
758
|
-
const MEDIA_STRING = 'media:
|
|
759
|
-
const MEDIA_INTEGER = 'media:integer;textable;numeric;scalar';
|
|
760
|
-
const MEDIA_NUMBER = 'media:
|
|
761
|
-
const MEDIA_BOOLEAN = 'media:
|
|
762
|
-
const MEDIA_OBJECT = 'media:
|
|
763
|
-
const MEDIA_STRING_ARRAY = 'media:
|
|
764
|
-
const MEDIA_INTEGER_ARRAY = 'media:integer
|
|
765
|
-
const MEDIA_NUMBER_ARRAY = 'media:
|
|
766
|
-
const MEDIA_BOOLEAN_ARRAY = 'media:
|
|
767
|
-
const MEDIA_OBJECT_ARRAY = 'media:
|
|
768
|
-
const MEDIA_BINARY = 'media:
|
|
758
|
+
const MEDIA_STRING = 'media:textable;form=scalar';
|
|
759
|
+
const MEDIA_INTEGER = 'media:integer;textable;numeric;form=scalar';
|
|
760
|
+
const MEDIA_NUMBER = 'media:textable;numeric;form=scalar';
|
|
761
|
+
const MEDIA_BOOLEAN = 'media:bool;textable;form=scalar';
|
|
762
|
+
const MEDIA_OBJECT = 'media:form=map;textable';
|
|
763
|
+
const MEDIA_STRING_ARRAY = 'media:textable;form=list';
|
|
764
|
+
const MEDIA_INTEGER_ARRAY = 'media:integer;textable;numeric;form=list';
|
|
765
|
+
const MEDIA_NUMBER_ARRAY = 'media:textable;numeric;form=list';
|
|
766
|
+
const MEDIA_BOOLEAN_ARRAY = 'media:bool;textable;form=list';
|
|
767
|
+
const MEDIA_OBJECT_ARRAY = 'media:form=list;textable';
|
|
768
|
+
const MEDIA_BINARY = 'media:bytes';
|
|
769
769
|
const MEDIA_VOID = 'media:void';
|
|
770
770
|
// Semantic content types
|
|
771
|
-
const MEDIA_PNG = 'media:png;
|
|
772
|
-
const MEDIA_AUDIO = 'media:wav;audio;
|
|
773
|
-
const MEDIA_VIDEO = 'media:video;
|
|
774
|
-
|
|
771
|
+
const MEDIA_PNG = 'media:image;png;bytes';
|
|
772
|
+
const MEDIA_AUDIO = 'media:wav;audio;bytes;';
|
|
773
|
+
const MEDIA_VIDEO = 'media:video;bytes';
|
|
774
|
+
// Semantic AI input types
|
|
775
|
+
const MEDIA_AUDIO_SPEECH = 'media:audio;wav;bytes;speech';
|
|
776
|
+
const MEDIA_IMAGE_THUMBNAIL = 'media:image;png;bytes;thumbnail';
|
|
775
777
|
// Document types (PRIMARY naming - type IS the format)
|
|
776
|
-
const MEDIA_PDF = 'media:pdf;
|
|
777
|
-
const MEDIA_EPUB = 'media:epub;
|
|
778
|
+
const MEDIA_PDF = 'media:pdf;bytes';
|
|
779
|
+
const MEDIA_EPUB = 'media:epub;bytes';
|
|
778
780
|
// Text format types (PRIMARY naming - type IS the format)
|
|
779
781
|
const MEDIA_MD = 'media:md;textable';
|
|
780
782
|
const MEDIA_TXT = 'media:txt;textable';
|
|
@@ -782,8 +784,16 @@ const MEDIA_RST = 'media:rst;textable';
|
|
|
782
784
|
const MEDIA_LOG = 'media:log;textable';
|
|
783
785
|
const MEDIA_HTML = 'media:html;textable';
|
|
784
786
|
const MEDIA_XML = 'media:xml;textable';
|
|
785
|
-
const MEDIA_JSON = 'media:json;textable;
|
|
786
|
-
const
|
|
787
|
+
const MEDIA_JSON = 'media:json;textable;form=map';
|
|
788
|
+
const MEDIA_JSON_SCHEMA = 'media:json;json-schema;textable;form=map';
|
|
789
|
+
const MEDIA_YAML = 'media:yaml;textable;form=map';
|
|
790
|
+
// Semantic input types
|
|
791
|
+
const MEDIA_MODEL_SPEC = 'media:model-spec;textable;form=scalar';
|
|
792
|
+
const MEDIA_MODEL_REPO = 'media:model-repo;textable;form=map';
|
|
793
|
+
// Semantic output types
|
|
794
|
+
const MEDIA_MODEL_DIM = 'media:model-dim;integer;textable;numeric;form=scalar';
|
|
795
|
+
const MEDIA_DECISION = 'media:decision;bool;textable;form=scalar';
|
|
796
|
+
const MEDIA_DECISION_ARRAY = 'media:decision;bool;textable;form=list';
|
|
787
797
|
|
|
788
798
|
// =============================================================================
|
|
789
799
|
// SCHEMA URL CONFIGURATION
|
|
@@ -851,7 +861,6 @@ const BUILTIN_SPECS = {
|
|
|
851
861
|
[MEDIA_PNG]: 'image/png; profile=https://capns.org/schema/image',
|
|
852
862
|
[MEDIA_AUDIO]: 'audio/wav; profile=https://capns.org/schema/audio',
|
|
853
863
|
[MEDIA_VIDEO]: 'video/mp4; profile=https://capns.org/schema/video',
|
|
854
|
-
[MEDIA_TEXT]: 'text/plain; profile=https://capns.org/schema/text',
|
|
855
864
|
// Document types (PRIMARY naming)
|
|
856
865
|
[MEDIA_PDF]: 'application/pdf',
|
|
857
866
|
[MEDIA_EPUB]: 'application/epub+zip',
|
|
@@ -867,7 +876,7 @@ const BUILTIN_SPECS = {
|
|
|
867
876
|
};
|
|
868
877
|
|
|
869
878
|
/**
|
|
870
|
-
* Check if a media URN has a marker tag (e.g.,
|
|
879
|
+
* Check if a media URN has a marker tag (e.g., bytes, json, textable).
|
|
871
880
|
* Uses TaggedUrn parsing for proper tag detection.
|
|
872
881
|
* @param {string} mediaUrn - The media URN
|
|
873
882
|
* @param {string} tagName - The marker tag name to check
|
|
@@ -879,6 +888,20 @@ function hasMediaUrnTag(mediaUrn, tagName) {
|
|
|
879
888
|
return parsed.getTag(tagName) !== undefined;
|
|
880
889
|
}
|
|
881
890
|
|
|
891
|
+
/**
|
|
892
|
+
* Check if a media URN has a tag with a specific value (e.g., form=map).
|
|
893
|
+
* Uses TaggedUrn parsing for proper tag detection.
|
|
894
|
+
* @param {string} mediaUrn - The media URN
|
|
895
|
+
* @param {string} tagName - The tag key to check
|
|
896
|
+
* @param {string} tagValue - The expected tag value
|
|
897
|
+
* @returns {boolean} True if the tag has the expected value
|
|
898
|
+
*/
|
|
899
|
+
function hasMediaUrnTagValue(mediaUrn, tagName, tagValue) {
|
|
900
|
+
if (!mediaUrn) return false;
|
|
901
|
+
const parsed = TaggedUrn.fromString(mediaUrn);
|
|
902
|
+
return parsed.getTag(tagName) === tagValue;
|
|
903
|
+
}
|
|
904
|
+
|
|
882
905
|
/**
|
|
883
906
|
* Parsed MediaSpec structure
|
|
884
907
|
*
|
|
@@ -996,23 +1019,63 @@ class MediaSpec {
|
|
|
996
1019
|
}
|
|
997
1020
|
|
|
998
1021
|
/**
|
|
999
|
-
* Check if this media spec represents binary
|
|
1000
|
-
* Returns true if the "
|
|
1022
|
+
* Check if this media spec represents binary data.
|
|
1023
|
+
* Returns true if the "bytes" marker tag is present in the source media URN.
|
|
1001
1024
|
* @returns {boolean} True if binary
|
|
1002
1025
|
*/
|
|
1003
1026
|
isBinary() {
|
|
1004
1027
|
if (!this.mediaUrn) return false;
|
|
1005
|
-
return hasMediaUrnTag(this.mediaUrn, '
|
|
1028
|
+
return hasMediaUrnTag(this.mediaUrn, 'bytes');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Check if this media spec represents a map/object structure (form=map).
|
|
1033
|
+
* This indicates a key-value structure, regardless of representation format.
|
|
1034
|
+
* @returns {boolean} True if map
|
|
1035
|
+
*/
|
|
1036
|
+
isMap() {
|
|
1037
|
+
if (!this.mediaUrn) return false;
|
|
1038
|
+
return hasMediaUrnTagValue(this.mediaUrn, 'form', 'map');
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Check if this media spec represents a scalar value (form=scalar).
|
|
1043
|
+
* @returns {boolean} True if scalar
|
|
1044
|
+
*/
|
|
1045
|
+
isScalar() {
|
|
1046
|
+
if (!this.mediaUrn) return false;
|
|
1047
|
+
return hasMediaUrnTagValue(this.mediaUrn, 'form', 'scalar');
|
|
1006
1048
|
}
|
|
1007
1049
|
|
|
1008
1050
|
/**
|
|
1009
|
-
* Check if this media spec represents
|
|
1010
|
-
*
|
|
1011
|
-
|
|
1051
|
+
* Check if this media spec represents a list/array structure (form=list).
|
|
1052
|
+
* @returns {boolean} True if list
|
|
1053
|
+
*/
|
|
1054
|
+
isList() {
|
|
1055
|
+
if (!this.mediaUrn) return false;
|
|
1056
|
+
return hasMediaUrnTagValue(this.mediaUrn, 'form', 'list');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Check if this media spec represents structured data (map or list).
|
|
1061
|
+
* Structured data can be serialized as JSON when transmitted as text.
|
|
1062
|
+
* Note: This does NOT check for the explicit `json` tag - use isJSON() for that.
|
|
1063
|
+
* @returns {boolean} True if structured (map or list)
|
|
1064
|
+
*/
|
|
1065
|
+
isStructured() {
|
|
1066
|
+
return this.isMap() || this.isList();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Check if this media spec represents JSON representation specifically.
|
|
1071
|
+
* Returns true if the "json" marker tag is present in the source media URN.
|
|
1072
|
+
* Note: This only checks for explicit JSON format marker.
|
|
1073
|
+
* For checking if data is structured (map/list), use isStructured().
|
|
1074
|
+
* @returns {boolean} True if JSON representation
|
|
1012
1075
|
*/
|
|
1013
1076
|
isJSON() {
|
|
1014
1077
|
if (!this.mediaUrn) return false;
|
|
1015
|
-
return hasMediaUrnTag(this.mediaUrn, '
|
|
1078
|
+
return hasMediaUrnTag(this.mediaUrn, 'json');
|
|
1016
1079
|
}
|
|
1017
1080
|
|
|
1018
1081
|
/**
|
|
@@ -1138,6 +1201,88 @@ function isBuiltinMediaUrn(mediaUrn) {
|
|
|
1138
1201
|
return BUILTIN_SPECS.hasOwnProperty(mediaUrn);
|
|
1139
1202
|
}
|
|
1140
1203
|
|
|
1204
|
+
/**
|
|
1205
|
+
* XV5: Validate that inline media_specs don't redefine built-in/registry specs.
|
|
1206
|
+
*
|
|
1207
|
+
* For capns-js (client-side), we check against BUILTIN_SPECS.
|
|
1208
|
+
* Server-side validation (capns_dot_org) should check against the full registry.
|
|
1209
|
+
*
|
|
1210
|
+
* @param {Object} mediaSpecs - The inline media_specs object from a capability
|
|
1211
|
+
* @param {Object} [options] - Validation options
|
|
1212
|
+
* @param {Function} [options.registryLookup] - Optional async function to check registry (for server-side)
|
|
1213
|
+
* @returns {Promise<{valid: boolean, error?: string, redefines?: string[]}>}
|
|
1214
|
+
*/
|
|
1215
|
+
async function validateNoMediaSpecRedefinition(mediaSpecs, options = {}) {
|
|
1216
|
+
if (!mediaSpecs || typeof mediaSpecs !== 'object' || Object.keys(mediaSpecs).length === 0) {
|
|
1217
|
+
return { valid: true };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const { registryLookup } = options;
|
|
1221
|
+
const redefines = [];
|
|
1222
|
+
|
|
1223
|
+
for (const mediaUrn of Object.keys(mediaSpecs)) {
|
|
1224
|
+
// Check against built-in specs first (always available)
|
|
1225
|
+
if (isBuiltinMediaUrn(mediaUrn)) {
|
|
1226
|
+
redefines.push(mediaUrn);
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// If a registry lookup function is provided (server-side), check against it
|
|
1231
|
+
if (registryLookup && typeof registryLookup === 'function') {
|
|
1232
|
+
try {
|
|
1233
|
+
const existsInRegistry = await registryLookup(mediaUrn);
|
|
1234
|
+
if (existsInRegistry) {
|
|
1235
|
+
redefines.push(mediaUrn);
|
|
1236
|
+
}
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
// Network/registry unavailable - log warning and allow (graceful degradation)
|
|
1239
|
+
console.warn(`[WARN] XV5: Could not verify inline spec '${mediaUrn}' against registry: ${err.message}. Allowing operation in offline mode.`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (redefines.length > 0) {
|
|
1245
|
+
return {
|
|
1246
|
+
valid: false,
|
|
1247
|
+
error: `XV5: Inline media specs redefine existing registry specs: ${redefines.join(', ')}`,
|
|
1248
|
+
redefines
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
return { valid: true };
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* XV5: Synchronous version for checking against built-in specs only.
|
|
1257
|
+
* Use this for client-side validation where registry lookup isn't available.
|
|
1258
|
+
*
|
|
1259
|
+
* @param {Object} mediaSpecs - The inline media_specs object from a capability
|
|
1260
|
+
* @returns {{valid: boolean, error?: string, redefines?: string[]}}
|
|
1261
|
+
*/
|
|
1262
|
+
function validateNoMediaSpecRedefinitionSync(mediaSpecs) {
|
|
1263
|
+
if (!mediaSpecs || typeof mediaSpecs !== 'object' || Object.keys(mediaSpecs).length === 0) {
|
|
1264
|
+
return { valid: true };
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const redefines = [];
|
|
1268
|
+
|
|
1269
|
+
for (const mediaUrn of Object.keys(mediaSpecs)) {
|
|
1270
|
+
if (isBuiltinMediaUrn(mediaUrn)) {
|
|
1271
|
+
redefines.push(mediaUrn);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (redefines.length > 0) {
|
|
1276
|
+
return {
|
|
1277
|
+
valid: false,
|
|
1278
|
+
error: `XV5: Inline media specs redefine existing built-in specs: ${redefines.join(', ')}`,
|
|
1279
|
+
redefines
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return { valid: true };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1141
1286
|
/**
|
|
1142
1287
|
* Check if a CapUrn represents binary output.
|
|
1143
1288
|
* Throws error if the output spec cannot be resolved - no fallbacks.
|
|
@@ -1153,10 +1298,11 @@ function isBinaryCapUrn(capUrn, mediaSpecs = {}) {
|
|
|
1153
1298
|
|
|
1154
1299
|
/**
|
|
1155
1300
|
* Check if a CapUrn represents JSON output.
|
|
1301
|
+
* Note: This checks for explicit JSON format marker only.
|
|
1156
1302
|
* Throws error if the output spec cannot be resolved - no fallbacks.
|
|
1157
1303
|
* @param {CapUrn} capUrn - The cap URN
|
|
1158
1304
|
* @param {Object} mediaSpecs - Optional mediaSpecs lookup table
|
|
1159
|
-
* @returns {boolean} True if JSON
|
|
1305
|
+
* @returns {boolean} True if explicit JSON tag present
|
|
1160
1306
|
* @throws {MediaSpecError} If 'out' tag is missing or spec ID cannot be resolved
|
|
1161
1307
|
*/
|
|
1162
1308
|
function isJSONCapUrn(capUrn, mediaSpecs = {}) {
|
|
@@ -1164,6 +1310,20 @@ function isJSONCapUrn(capUrn, mediaSpecs = {}) {
|
|
|
1164
1310
|
return mediaSpec.isJSON();
|
|
1165
1311
|
}
|
|
1166
1312
|
|
|
1313
|
+
/**
|
|
1314
|
+
* Check if a CapUrn represents structured output (map or list).
|
|
1315
|
+
* Structured data can be serialized as JSON when transmitted as text.
|
|
1316
|
+
* Throws error if the output spec cannot be resolved - no fallbacks.
|
|
1317
|
+
* @param {CapUrn} capUrn - The cap URN
|
|
1318
|
+
* @param {Object} mediaSpecs - Optional mediaSpecs lookup table
|
|
1319
|
+
* @returns {boolean} True if structured (map or list)
|
|
1320
|
+
* @throws {MediaSpecError} If 'out' tag is missing or spec ID cannot be resolved
|
|
1321
|
+
*/
|
|
1322
|
+
function isStructuredCapUrn(capUrn, mediaSpecs = {}) {
|
|
1323
|
+
const mediaSpec = MediaSpec.fromCapUrn(capUrn, mediaSpecs);
|
|
1324
|
+
return mediaSpec.isStructured();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1167
1327
|
/**
|
|
1168
1328
|
* Registration attribution - who registered a capability and when
|
|
1169
1329
|
*/
|
|
@@ -1758,7 +1918,7 @@ class ValidationError extends Error {
|
|
|
1758
1918
|
return `Cap '${capUrn}' argument '${details.argumentName}' expects media_spec '${details.expectedMediaSpec}' but ${errors} for value: ${JSON.stringify(details.actualValue)}`;
|
|
1759
1919
|
}
|
|
1760
1920
|
return `Cap '${capUrn}' argument '${details.argumentName}' expects type '${details.expectedType}' but received '${details.actualType}' with value: ${JSON.stringify(details.actualValue)}`;
|
|
1761
|
-
case '
|
|
1921
|
+
case 'MediaValidationFailed':
|
|
1762
1922
|
return `Cap '${capUrn}' argument '${details.argumentName}' failed validation rule '${details.validationRule}' with value: ${JSON.stringify(details.actualValue)}`;
|
|
1763
1923
|
case 'MediaSpecValidationFailed':
|
|
1764
1924
|
return `Cap '${capUrn}' argument '${details.argumentName}' failed media spec '${details.mediaUrn}' validation rule '${details.validationRule}' with value: ${JSON.stringify(details.actualValue)}`;
|
|
@@ -3346,8 +3506,11 @@ module.exports = {
|
|
|
3346
3506
|
MediaSpecErrorCodes,
|
|
3347
3507
|
isBinaryCapUrn,
|
|
3348
3508
|
isJSONCapUrn,
|
|
3509
|
+
isStructuredCapUrn,
|
|
3349
3510
|
resolveMediaUrn,
|
|
3350
3511
|
isBuiltinMediaUrn,
|
|
3512
|
+
validateNoMediaSpecRedefinition,
|
|
3513
|
+
validateNoMediaSpecRedefinitionSync,
|
|
3351
3514
|
BUILTIN_SPECS,
|
|
3352
3515
|
getSchemaBaseURL,
|
|
3353
3516
|
getProfileURL,
|
|
@@ -3366,7 +3529,8 @@ module.exports = {
|
|
|
3366
3529
|
MEDIA_PNG,
|
|
3367
3530
|
MEDIA_AUDIO,
|
|
3368
3531
|
MEDIA_VIDEO,
|
|
3369
|
-
|
|
3532
|
+
MEDIA_AUDIO_SPEECH,
|
|
3533
|
+
MEDIA_IMAGE_THUMBNAIL,
|
|
3370
3534
|
// Document types (PRIMARY naming)
|
|
3371
3535
|
MEDIA_PDF,
|
|
3372
3536
|
MEDIA_EPUB,
|
|
@@ -3378,7 +3542,13 @@ module.exports = {
|
|
|
3378
3542
|
MEDIA_HTML,
|
|
3379
3543
|
MEDIA_XML,
|
|
3380
3544
|
MEDIA_JSON,
|
|
3545
|
+
MEDIA_JSON_SCHEMA,
|
|
3381
3546
|
MEDIA_YAML,
|
|
3547
|
+
MEDIA_MODEL_SPEC,
|
|
3548
|
+
MEDIA_MODEL_REPO,
|
|
3549
|
+
MEDIA_MODEL_DIM,
|
|
3550
|
+
MEDIA_DECISION,
|
|
3551
|
+
MEDIA_DECISION_ARRAY,
|
|
3382
3552
|
CapMatrixError,
|
|
3383
3553
|
CapMatrix,
|
|
3384
3554
|
BestCapSetMatch,
|
package/capns.test.js
CHANGED
|
@@ -33,7 +33,9 @@ const {
|
|
|
33
33
|
CapGraph,
|
|
34
34
|
// StdinSource
|
|
35
35
|
StdinSource,
|
|
36
|
-
StdinSourceKind
|
|
36
|
+
StdinSourceKind,
|
|
37
|
+
// XV5 validation
|
|
38
|
+
validateNoMediaSpecRedefinitionSync
|
|
37
39
|
} = require('./capns.js');
|
|
38
40
|
|
|
39
41
|
// Test assertion utility
|
|
@@ -68,13 +70,14 @@ function assertThrows(fn, expectedErrorCode, message) {
|
|
|
68
70
|
*/
|
|
69
71
|
function testUrn(tags) {
|
|
70
72
|
if (!tags || tags === '') {
|
|
71
|
-
return 'cap:in="media:void";out="media:
|
|
73
|
+
return 'cap:in="media:void";out="media:form=map"';
|
|
72
74
|
}
|
|
73
|
-
return 'cap:in="media:void";out="media:
|
|
75
|
+
return 'cap:in="media:void";out="media:form=map";' + tags;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
// Test suite - defined at the end of file
|
|
77
79
|
|
|
80
|
+
// TEST001: Test that cap URN is created with tags parsed correctly and direction specs accessible
|
|
78
81
|
function testCapUrnCreation() {
|
|
79
82
|
console.log('Testing Cap URN creation...');
|
|
80
83
|
|
|
@@ -83,16 +86,17 @@ function testCapUrnCreation() {
|
|
|
83
86
|
assertEqual(cap.getTag('target'), 'thumbnail', 'Should get target tag');
|
|
84
87
|
assertEqual(cap.getTag('ext'), 'pdf', 'Should get ext tag');
|
|
85
88
|
assertEqual(cap.getInSpec(), 'media:void', 'Should get inSpec');
|
|
86
|
-
assertEqual(cap.getOutSpec(), 'media:
|
|
89
|
+
assertEqual(cap.getOutSpec(), 'media:form=map', 'Should get outSpec');
|
|
87
90
|
|
|
88
91
|
console.log(' ✓ Cap URN creation');
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
// TEST002: Test that tag keys and values are normalized to lowercase for case-insensitive comparison
|
|
91
95
|
function testCaseInsensitive() {
|
|
92
96
|
console.log('Testing case insensitive behavior...');
|
|
93
97
|
|
|
94
98
|
// Test that different casing produces the same URN
|
|
95
|
-
const cap1 = CapUrn.fromString('cap:IN="media:void";OUT="media:
|
|
99
|
+
const cap1 = CapUrn.fromString('cap:IN="media:void";OUT="media:form=map";OP=Generate;EXT=PDF;Target=Thumbnail');
|
|
96
100
|
const cap2 = CapUrn.fromString(testUrn('op=generate;ext=pdf;target=thumbnail'));
|
|
97
101
|
|
|
98
102
|
// Both should be normalized to lowercase
|
|
@@ -114,7 +118,7 @@ function testCaseInsensitive() {
|
|
|
114
118
|
|
|
115
119
|
// Case-insensitive in/out lookup
|
|
116
120
|
assertEqual(cap1.getTag('IN'), 'media:void', 'Should lookup in with case-insensitive key');
|
|
117
|
-
assertEqual(cap1.getTag('OUT'), 'media:
|
|
121
|
+
assertEqual(cap1.getTag('OUT'), 'media:form=map', 'Should lookup out with case-insensitive key');
|
|
118
122
|
|
|
119
123
|
// Matching should work case-insensitively
|
|
120
124
|
assert(cap1.matches(cap2), 'Should match case-insensitively');
|
|
@@ -123,6 +127,7 @@ function testCaseInsensitive() {
|
|
|
123
127
|
console.log(' ✓ Case insensitive behavior');
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
// TEST003: Test that URN without cap prefix is rejected with MISSING_CAP_PREFIX error
|
|
126
131
|
function testCapPrefixRequired() {
|
|
127
132
|
console.log('Testing cap: prefix requirement...');
|
|
128
133
|
|
|
@@ -140,6 +145,7 @@ function testCapPrefixRequired() {
|
|
|
140
145
|
console.log(' ✓ Cap prefix requirement');
|
|
141
146
|
}
|
|
142
147
|
|
|
148
|
+
// TEST004: Test that URNs with and without trailing semicolon are equivalent
|
|
143
149
|
function testTrailingSemicolonEquivalence() {
|
|
144
150
|
console.log('Testing trailing semicolon equivalence...');
|
|
145
151
|
|
|
@@ -160,18 +166,20 @@ function testTrailingSemicolonEquivalence() {
|
|
|
160
166
|
console.log(' ✓ Trailing semicolon equivalence');
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
// TEST005: Test that toString produces canonical form with alphabetically sorted tags
|
|
163
170
|
function testCanonicalStringFormat() {
|
|
164
171
|
console.log('Testing canonical string format...');
|
|
165
172
|
|
|
166
173
|
const cap = CapUrn.fromString(testUrn('op=generate;target=thumbnail;ext=pdf'));
|
|
167
174
|
// Should be sorted alphabetically and have no trailing semicolon in canonical form
|
|
168
175
|
// in/out are included in alphabetical order: 'ext' < 'in' < 'op' < 'out' < 'target'
|
|
169
|
-
//
|
|
170
|
-
assertEqual(cap.toString(), 'cap:ext=pdf;in=media:void;op=generate;out=media:
|
|
176
|
+
// Values with '=' need quoting - media:form=map requires quotes
|
|
177
|
+
assertEqual(cap.toString(), 'cap:ext=pdf;in=media:void;op=generate;out="media:form=map";target=thumbnail', 'Should be alphabetically sorted');
|
|
171
178
|
|
|
172
179
|
console.log(' ✓ Canonical string format');
|
|
173
180
|
}
|
|
174
181
|
|
|
182
|
+
// TEST006: Test that cap matches request with exact tags, subset tags, and wildcards
|
|
175
183
|
function testTagMatching() {
|
|
176
184
|
console.log('Testing tag matching...');
|
|
177
185
|
|
|
@@ -200,6 +208,7 @@ function testTagMatching() {
|
|
|
200
208
|
console.log(' ✓ Tag matching');
|
|
201
209
|
}
|
|
202
210
|
|
|
211
|
+
// TEST007: Test that missing tags are treated as wildcards for matching
|
|
203
212
|
function testMissingTagHandling() {
|
|
204
213
|
console.log('Testing missing tag handling...');
|
|
205
214
|
|
|
@@ -217,6 +226,7 @@ function testMissingTagHandling() {
|
|
|
217
226
|
console.log(' ✓ Missing tag handling');
|
|
218
227
|
}
|
|
219
228
|
|
|
229
|
+
// TEST008: Test that specificity counts non-wildcard tags including in and out
|
|
220
230
|
function testSpecificity() {
|
|
221
231
|
console.log('Testing specificity...');
|
|
222
232
|
|
|
@@ -239,6 +249,7 @@ function testSpecificity() {
|
|
|
239
249
|
console.log(' ✓ Specificity');
|
|
240
250
|
}
|
|
241
251
|
|
|
252
|
+
// TEST009: Test that compatibility checks if caps can handle same requests
|
|
242
253
|
function testCompatibility() {
|
|
243
254
|
console.log('Testing compatibility...');
|
|
244
255
|
|
|
@@ -262,6 +273,7 @@ function testCompatibility() {
|
|
|
262
273
|
console.log(' ✓ Compatibility');
|
|
263
274
|
}
|
|
264
275
|
|
|
276
|
+
// TEST010: Test that CapUrnBuilder creates valid URN with tags and direction specs
|
|
265
277
|
function testBuilder() {
|
|
266
278
|
console.log('Testing builder...');
|
|
267
279
|
|
|
@@ -295,6 +307,7 @@ function testBuilder() {
|
|
|
295
307
|
console.log(' ✓ Builder');
|
|
296
308
|
}
|
|
297
309
|
|
|
310
|
+
// TEST011: Test convenience methods withTag, withoutTag, withInSpec, withOutSpec, merge, subset, withWildcardTag
|
|
298
311
|
function testConvenienceMethods() {
|
|
299
312
|
console.log('Testing convenience methods...');
|
|
300
313
|
|
|
@@ -305,7 +318,7 @@ function testConvenienceMethods() {
|
|
|
305
318
|
assertEqual(modified.getTag('op'), 'generate', 'Should preserve original tag');
|
|
306
319
|
assertEqual(modified.getTag('ext'), 'pdf', 'Should add new tag');
|
|
307
320
|
assertEqual(modified.getInSpec(), 'media:void', 'Should preserve inSpec');
|
|
308
|
-
assertEqual(modified.getOutSpec(), 'media:
|
|
321
|
+
assertEqual(modified.getOutSpec(), 'media:form=map', 'Should preserve outSpec');
|
|
309
322
|
|
|
310
323
|
// Test withTag silently ignores in/out
|
|
311
324
|
const modified2 = original.withTag('in', 'media:string');
|
|
@@ -357,6 +370,7 @@ function testConvenienceMethods() {
|
|
|
357
370
|
console.log(' ✓ Convenience methods');
|
|
358
371
|
}
|
|
359
372
|
|
|
373
|
+
// TEST012: Test that CapMatcher finds best and all matches from cap list
|
|
360
374
|
function testCapMatcher() {
|
|
361
375
|
console.log('Testing CapMatcher...');
|
|
362
376
|
|
|
@@ -371,7 +385,8 @@ function testCapMatcher() {
|
|
|
371
385
|
|
|
372
386
|
// Most specific cap that can handle the request (ext=pdf is more specific)
|
|
373
387
|
// Canonical order is alphabetical: ext, in, op, out
|
|
374
|
-
|
|
388
|
+
// Note: media:form=map needs quotes because it contains '='
|
|
389
|
+
assertEqual(best.toString(), 'cap:ext=pdf;in=media:void;op=generate;out="media:form=map"', 'Should find most specific match');
|
|
375
390
|
|
|
376
391
|
// Test findAllMatches - now only 2 match because first has wildcard in/out
|
|
377
392
|
const matches = CapMatcher.findAllMatches(caps, request);
|
|
@@ -382,6 +397,7 @@ function testCapMatcher() {
|
|
|
382
397
|
console.log(' ✓ CapMatcher');
|
|
383
398
|
}
|
|
384
399
|
|
|
400
|
+
// TEST013: Test that cap URN serializes to and deserializes from JSON
|
|
385
401
|
function testJSONSerialization() {
|
|
386
402
|
console.log('Testing JSON serialization...');
|
|
387
403
|
|
|
@@ -392,11 +408,12 @@ function testJSONSerialization() {
|
|
|
392
408
|
|
|
393
409
|
assert(original.equals(restored), 'Should serialize/deserialize correctly');
|
|
394
410
|
assertEqual(restored.getInSpec(), 'media:void', 'Should preserve inSpec');
|
|
395
|
-
assertEqual(restored.getOutSpec(), 'media:
|
|
411
|
+
assertEqual(restored.getOutSpec(), 'media:form=map', 'Should preserve outSpec');
|
|
396
412
|
|
|
397
413
|
console.log(' ✓ JSON serialization');
|
|
398
414
|
}
|
|
399
415
|
|
|
416
|
+
// TEST014: Test that empty cap URN without in/out fails, minimal URN with just in/out succeeds
|
|
400
417
|
function testEmptyCapUrn() {
|
|
401
418
|
console.log('Testing empty cap URN now fails (in/out required)...');
|
|
402
419
|
|
|
@@ -418,7 +435,7 @@ function testEmptyCapUrn() {
|
|
|
418
435
|
const minimal = CapUrn.fromString(testUrn(''));
|
|
419
436
|
assertEqual(Object.keys(minimal.tags).length, 0, 'Should have no other tags');
|
|
420
437
|
assertEqual(minimal.getInSpec(), 'media:void', 'Should have inSpec');
|
|
421
|
-
assertEqual(minimal.getOutSpec(), 'media:
|
|
438
|
+
assertEqual(minimal.getOutSpec(), 'media:form=map', 'Should have outSpec');
|
|
422
439
|
|
|
423
440
|
// For "match anything" behavior, use wildcards
|
|
424
441
|
const wildcard = CapUrn.fromString('cap:in=*;out=*');
|
|
@@ -429,6 +446,7 @@ function testEmptyCapUrn() {
|
|
|
429
446
|
console.log(' ✓ Empty cap URN now fails (in/out required)');
|
|
430
447
|
}
|
|
431
448
|
|
|
449
|
+
// TEST015: Test that URN supports forward slashes and colons in tag values
|
|
432
450
|
function testExtendedCharacterSupport() {
|
|
433
451
|
console.log('Testing extended character support...');
|
|
434
452
|
|
|
@@ -440,6 +458,7 @@ function testExtendedCharacterSupport() {
|
|
|
440
458
|
console.log(' ✓ Extended character support');
|
|
441
459
|
}
|
|
442
460
|
|
|
461
|
+
// TEST016: Test that wildcard is rejected in keys but accepted in values
|
|
443
462
|
function testWildcardRestrictions() {
|
|
444
463
|
console.log('Testing wildcard restrictions...');
|
|
445
464
|
|
|
@@ -462,6 +481,7 @@ function testWildcardRestrictions() {
|
|
|
462
481
|
console.log(' ✓ Wildcard restrictions');
|
|
463
482
|
}
|
|
464
483
|
|
|
484
|
+
// TEST017: Test that duplicate keys in URN are rejected with DUPLICATE_KEY error
|
|
465
485
|
function testDuplicateKeyRejection() {
|
|
466
486
|
console.log('Testing duplicate key rejection...');
|
|
467
487
|
|
|
@@ -475,6 +495,7 @@ function testDuplicateKeyRejection() {
|
|
|
475
495
|
console.log(' ✓ Duplicate key rejection');
|
|
476
496
|
}
|
|
477
497
|
|
|
498
|
+
// TEST018: Test that pure numeric keys are rejected but mixed alphanumeric keys are allowed
|
|
478
499
|
function testNumericKeyRestriction() {
|
|
479
500
|
console.log('Testing numeric key restriction...');
|
|
480
501
|
|
|
@@ -503,6 +524,7 @@ function testNumericKeyRestriction() {
|
|
|
503
524
|
// NEW FORMAT TESTS - Spec ID Resolution and MediaSpec
|
|
504
525
|
// ============================================================================
|
|
505
526
|
|
|
527
|
+
// TEST057: Test MediaSpec parses canonical format without content-type prefix
|
|
506
528
|
function testMediaSpecCanonicalFormat() {
|
|
507
529
|
console.log('Testing MediaSpec canonical format...');
|
|
508
530
|
|
|
@@ -523,6 +545,7 @@ function testMediaSpecCanonicalFormat() {
|
|
|
523
545
|
console.log(' ✓ MediaSpec canonical format');
|
|
524
546
|
}
|
|
525
547
|
|
|
548
|
+
// TEST058: Test MediaSpec fails hard on legacy content-type prefix format
|
|
526
549
|
function testMediaSpecLegacyFormatRejection() {
|
|
527
550
|
console.log('Testing legacy format rejection...');
|
|
528
551
|
|
|
@@ -542,6 +565,7 @@ function testMediaSpecLegacyFormatRejection() {
|
|
|
542
565
|
console.log(' ✓ Legacy format rejection');
|
|
543
566
|
}
|
|
544
567
|
|
|
568
|
+
// TEST059: Test built-in media URNs are recognized by isBuiltinMediaUrn
|
|
545
569
|
function testBuiltinSpecIds() {
|
|
546
570
|
console.log('Testing built-in spec IDs...');
|
|
547
571
|
|
|
@@ -559,6 +583,7 @@ function testBuiltinSpecIds() {
|
|
|
559
583
|
console.log(' ✓ Built-in spec IDs');
|
|
560
584
|
}
|
|
561
585
|
|
|
586
|
+
// TEST060: Test resolveMediaUrn resolves built-in media URNs to MediaSpec
|
|
562
587
|
function testSpecIdResolution() {
|
|
563
588
|
console.log('Testing spec ID resolution...');
|
|
564
589
|
|
|
@@ -581,6 +606,7 @@ function testSpecIdResolution() {
|
|
|
581
606
|
console.log(' ✓ Spec ID resolution');
|
|
582
607
|
}
|
|
583
608
|
|
|
609
|
+
// TEST061: Test resolveMediaUrn resolves custom media URNs from mediaSpecs table
|
|
584
610
|
function testMediaUrnResolutionWithMediaSpecs() {
|
|
585
611
|
console.log('Testing media URN resolution with custom mediaSpecs...');
|
|
586
612
|
|
|
@@ -612,6 +638,7 @@ function testMediaUrnResolutionWithMediaSpecs() {
|
|
|
612
638
|
console.log(' ✓ Media URN resolution with custom mediaSpecs');
|
|
613
639
|
}
|
|
614
640
|
|
|
641
|
+
// TEST062: Test resolveMediaUrn fails hard on unresolvable media URN
|
|
615
642
|
function testMediaUrnResolutionFailHard() {
|
|
616
643
|
console.log('Testing media URN resolution fail hard...');
|
|
617
644
|
|
|
@@ -631,6 +658,7 @@ function testMediaUrnResolutionFailHard() {
|
|
|
631
658
|
console.log(' ✓ Media URN resolution fail hard');
|
|
632
659
|
}
|
|
633
660
|
|
|
661
|
+
// TEST063: Test metadata is propagated from object form media spec definition
|
|
634
662
|
function testMetadataPropagation() {
|
|
635
663
|
console.log('Testing metadata propagation...');
|
|
636
664
|
|
|
@@ -661,6 +689,7 @@ function testMetadataPropagation() {
|
|
|
661
689
|
console.log(' ✓ Metadata propagation from object definition');
|
|
662
690
|
}
|
|
663
691
|
|
|
692
|
+
// TEST064: Test string form media spec definition has no metadata
|
|
664
693
|
function testMetadataForStringDef() {
|
|
665
694
|
console.log('Testing metadata for string definition...');
|
|
666
695
|
|
|
@@ -675,6 +704,7 @@ function testMetadataForStringDef() {
|
|
|
675
704
|
console.log(' ✓ String form has no metadata');
|
|
676
705
|
}
|
|
677
706
|
|
|
707
|
+
// TEST065: Test built-in media URNs have no metadata
|
|
678
708
|
function testMetadataForBuiltin() {
|
|
679
709
|
console.log('Testing metadata for built-in...');
|
|
680
710
|
|
|
@@ -685,6 +715,7 @@ function testMetadataForBuiltin() {
|
|
|
685
715
|
console.log(' ✓ Built-in has no metadata');
|
|
686
716
|
}
|
|
687
717
|
|
|
718
|
+
// TEST066: Test metadata and validation coexist in media spec definition
|
|
688
719
|
function testMetadataWithValidation() {
|
|
689
720
|
console.log('Testing metadata with validation...');
|
|
690
721
|
|
|
@@ -720,6 +751,7 @@ function testMetadataWithValidation() {
|
|
|
720
751
|
console.log(' ✓ Metadata coexists with validation');
|
|
721
752
|
}
|
|
722
753
|
|
|
754
|
+
// TEST108: Test Cap with mediaSpecs resolves custom and built-in media URNs
|
|
723
755
|
function testCapWithMediaSpecs() {
|
|
724
756
|
console.log('Testing Cap with mediaSpecs...');
|
|
725
757
|
|
|
@@ -754,6 +786,7 @@ function testCapWithMediaSpecs() {
|
|
|
754
786
|
console.log(' ✓ Cap with mediaSpecs');
|
|
755
787
|
}
|
|
756
788
|
|
|
789
|
+
// TEST109: Test Cap JSON serialization includes mediaSpecs and direction specs
|
|
757
790
|
function testCapJSONSerialization() {
|
|
758
791
|
console.log('Testing Cap JSON serialization with mediaSpecs...');
|
|
759
792
|
|
|
@@ -774,18 +807,19 @@ function testCapJSONSerialization() {
|
|
|
774
807
|
assertEqual(json.media_specs['media:custom'], 'text/plain; profile=https://example.com', 'Should serialize mediaSpecs');
|
|
775
808
|
// URN tags should include in and out
|
|
776
809
|
assertEqual(json.urn.tags['in'], 'media:void', 'Should serialize inSpec in tags');
|
|
777
|
-
assertEqual(json.urn.tags['out'], 'media:
|
|
810
|
+
assertEqual(json.urn.tags['out'], 'media:form=map', 'Should serialize outSpec in tags');
|
|
778
811
|
|
|
779
812
|
// Deserialize from JSON
|
|
780
813
|
const restored = Cap.fromJSON(json);
|
|
781
814
|
assert(restored.mediaSpecs !== undefined, 'Should restore mediaSpecs');
|
|
782
815
|
assertEqual(restored.mediaSpecs['media:custom'], 'text/plain; profile=https://example.com', 'Should restore mediaSpecs content');
|
|
783
816
|
assertEqual(restored.urn.getInSpec(), 'media:void', 'Should restore inSpec');
|
|
784
|
-
assertEqual(restored.urn.getOutSpec(), 'media:
|
|
817
|
+
assertEqual(restored.urn.getOutSpec(), 'media:form=map', 'Should restore outSpec');
|
|
785
818
|
|
|
786
819
|
console.log(' ✓ Cap JSON serialization with mediaSpecs');
|
|
787
820
|
}
|
|
788
821
|
|
|
822
|
+
// TEST019: Test op tag is used instead of deprecated action tag
|
|
789
823
|
function testOpTagRename() {
|
|
790
824
|
console.log('Testing op tag (renamed from action)...');
|
|
791
825
|
|
|
@@ -812,6 +846,7 @@ function testOpTagRename() {
|
|
|
812
846
|
// All implementations (Rust, Go, JS, ObjC) must pass these identically
|
|
813
847
|
// ============================================================================
|
|
814
848
|
|
|
849
|
+
// TEST020: Test matching semantics - exact match including in/out
|
|
815
850
|
function testMatchingSemantics_Test1_ExactMatch() {
|
|
816
851
|
console.log('Testing Matching Semantics Test 1: Exact match...');
|
|
817
852
|
// Test 1: Exact match (including in/out)
|
|
@@ -824,6 +859,7 @@ function testMatchingSemantics_Test1_ExactMatch() {
|
|
|
824
859
|
console.log(' ✓ Test 1: Exact match');
|
|
825
860
|
}
|
|
826
861
|
|
|
862
|
+
// TEST021: Test matching semantics - cap missing tag treated as implicit wildcard
|
|
827
863
|
function testMatchingSemantics_Test2_CapMissingTag() {
|
|
828
864
|
console.log('Testing Matching Semantics Test 2: Cap missing tag...');
|
|
829
865
|
// Test 2: Cap missing tag (implicit wildcard for other tags, not in/out)
|
|
@@ -836,6 +872,7 @@ function testMatchingSemantics_Test2_CapMissingTag() {
|
|
|
836
872
|
console.log(' ✓ Test 2: Cap missing tag');
|
|
837
873
|
}
|
|
838
874
|
|
|
875
|
+
// TEST022: Test matching semantics - cap with extra tag matches request
|
|
839
876
|
function testMatchingSemantics_Test3_CapHasExtraTag() {
|
|
840
877
|
console.log('Testing Matching Semantics Test 3: Cap has extra tag...');
|
|
841
878
|
// Test 3: Cap has extra tag
|
|
@@ -848,6 +885,7 @@ function testMatchingSemantics_Test3_CapHasExtraTag() {
|
|
|
848
885
|
console.log(' ✓ Test 3: Cap has extra tag');
|
|
849
886
|
}
|
|
850
887
|
|
|
888
|
+
// TEST023: Test matching semantics - request with wildcard matches specific cap
|
|
851
889
|
function testMatchingSemantics_Test4_RequestHasWildcard() {
|
|
852
890
|
console.log('Testing Matching Semantics Test 4: Request has wildcard...');
|
|
853
891
|
// Test 4: Request has wildcard
|
|
@@ -860,6 +898,7 @@ function testMatchingSemantics_Test4_RequestHasWildcard() {
|
|
|
860
898
|
console.log(' ✓ Test 4: Request has wildcard');
|
|
861
899
|
}
|
|
862
900
|
|
|
901
|
+
// TEST024: Test matching semantics - cap with wildcard matches specific request
|
|
863
902
|
function testMatchingSemantics_Test5_CapHasWildcard() {
|
|
864
903
|
console.log('Testing Matching Semantics Test 5: Cap has wildcard...');
|
|
865
904
|
// Test 5: Cap has wildcard
|
|
@@ -872,6 +911,7 @@ function testMatchingSemantics_Test5_CapHasWildcard() {
|
|
|
872
911
|
console.log(' ✓ Test 5: Cap has wildcard');
|
|
873
912
|
}
|
|
874
913
|
|
|
914
|
+
// TEST025: Test matching semantics - value mismatch does not match
|
|
875
915
|
function testMatchingSemantics_Test6_ValueMismatch() {
|
|
876
916
|
console.log('Testing Matching Semantics Test 6: Value mismatch...');
|
|
877
917
|
// Test 6: Value mismatch
|
|
@@ -884,6 +924,7 @@ function testMatchingSemantics_Test6_ValueMismatch() {
|
|
|
884
924
|
console.log(' ✓ Test 6: Value mismatch');
|
|
885
925
|
}
|
|
886
926
|
|
|
927
|
+
// TEST026: Test matching semantics - fallback pattern with missing tag as implicit wildcard
|
|
887
928
|
function testMatchingSemantics_Test7_FallbackPattern() {
|
|
888
929
|
console.log('Testing Matching Semantics Test 7: Fallback pattern...');
|
|
889
930
|
// Test 7: Fallback pattern
|
|
@@ -896,6 +937,7 @@ function testMatchingSemantics_Test7_FallbackPattern() {
|
|
|
896
937
|
console.log(' ✓ Test 7: Fallback pattern');
|
|
897
938
|
}
|
|
898
939
|
|
|
940
|
+
// TEST027: Test matching semantics - wildcard cap with in=* out=* matches anything
|
|
899
941
|
function testMatchingSemantics_Test8_WildcardCapMatchesAnything() {
|
|
900
942
|
console.log('Testing Matching Semantics Test 8: Wildcard cap matches anything...');
|
|
901
943
|
// Test 8: Wildcard cap matches anything (replaces empty cap test)
|
|
@@ -908,6 +950,7 @@ function testMatchingSemantics_Test8_WildcardCapMatchesAnything() {
|
|
|
908
950
|
console.log(' ✓ Test 8: Wildcard cap matches anything');
|
|
909
951
|
}
|
|
910
952
|
|
|
953
|
+
// TEST028: Test matching semantics - cross-dimension independence for other tags
|
|
911
954
|
function testMatchingSemantics_Test9_CrossDimensionIndependence() {
|
|
912
955
|
console.log('Testing Matching Semantics Test 9: Cross-dimension independence...');
|
|
913
956
|
// Test 9: Cross-dimension independence (for other tags, not in/out)
|
|
@@ -920,6 +963,7 @@ function testMatchingSemantics_Test9_CrossDimensionIndependence() {
|
|
|
920
963
|
console.log(' ✓ Test 9: Cross-dimension independence');
|
|
921
964
|
}
|
|
922
965
|
|
|
966
|
+
// TEST029: Test matching semantics - direction mismatch in/out does not match
|
|
923
967
|
function testMatchingSemantics_Test10_DirectionMismatch() {
|
|
924
968
|
console.log('Testing Matching Semantics Test 10: Direction mismatch...');
|
|
925
969
|
// Test 10: Direction mismatch (in/out must match)
|
|
@@ -964,6 +1008,7 @@ function makeCap(urnString, title) {
|
|
|
964
1008
|
return new Cap(capUrn, title, 'test', title);
|
|
965
1009
|
}
|
|
966
1010
|
|
|
1011
|
+
// TEST117: Test CapCube finds more specific cap across registries
|
|
967
1012
|
function testCapCubeMoreSpecificWins() {
|
|
968
1013
|
console.log('Testing CapCube: More specific wins...');
|
|
969
1014
|
|
|
@@ -1002,6 +1047,7 @@ function testCapCubeMoreSpecificWins() {
|
|
|
1002
1047
|
console.log(' ✓ More specific wins');
|
|
1003
1048
|
}
|
|
1004
1049
|
|
|
1050
|
+
// TEST118: Test CapCube tie-breaking prefers first registry in order
|
|
1005
1051
|
function testCapCubeTieGoesToFirst() {
|
|
1006
1052
|
console.log('Testing CapCube: Tie goes to first...');
|
|
1007
1053
|
|
|
@@ -1029,6 +1075,7 @@ function testCapCubeTieGoesToFirst() {
|
|
|
1029
1075
|
console.log(' ✓ Tie goes to first');
|
|
1030
1076
|
}
|
|
1031
1077
|
|
|
1078
|
+
// TEST119: Test CapCube polls all registries to find best match
|
|
1032
1079
|
function testCapCubePollsAll() {
|
|
1033
1080
|
console.log('Testing CapCube: Polls all registries...');
|
|
1034
1081
|
|
|
@@ -1063,6 +1110,7 @@ function testCapCubePollsAll() {
|
|
|
1063
1110
|
console.log(' ✓ Polls all registries');
|
|
1064
1111
|
}
|
|
1065
1112
|
|
|
1113
|
+
// TEST120: Test CapCube returns error when no cap matches request
|
|
1066
1114
|
function testCapCubeNoMatch() {
|
|
1067
1115
|
console.log('Testing CapCube: No match error...');
|
|
1068
1116
|
|
|
@@ -1081,6 +1129,7 @@ function testCapCubeNoMatch() {
|
|
|
1081
1129
|
console.log(' ✓ No match error');
|
|
1082
1130
|
}
|
|
1083
1131
|
|
|
1132
|
+
// TEST121: Test CapCube fallback scenario where generic cap handles unknown file types
|
|
1084
1133
|
function testCapCubeFallbackScenario() {
|
|
1085
1134
|
console.log('Testing CapCube: Fallback scenario...');
|
|
1086
1135
|
|
|
@@ -1125,6 +1174,7 @@ function testCapCubeFallbackScenario() {
|
|
|
1125
1174
|
console.log(' ✓ Fallback scenario');
|
|
1126
1175
|
}
|
|
1127
1176
|
|
|
1177
|
+
// TEST122: Test CapCube can method returns execution info and canHandle checks capability
|
|
1128
1178
|
function testCapCubeCanMethod() {
|
|
1129
1179
|
console.log('Testing CapCube: can() method...');
|
|
1130
1180
|
|
|
@@ -1149,6 +1199,7 @@ function testCapCubeCanMethod() {
|
|
|
1149
1199
|
console.log(' ✓ can() method');
|
|
1150
1200
|
}
|
|
1151
1201
|
|
|
1202
|
+
// TEST123: Test CapCube registry management add, get, remove operations
|
|
1152
1203
|
function testCapCubeRegistryManagement() {
|
|
1153
1204
|
console.log('Testing CapCube: Registry management...');
|
|
1154
1205
|
|
|
@@ -1190,6 +1241,7 @@ function makeGraphCap(inUrn, outUrn, title) {
|
|
|
1190
1241
|
return new Cap(capUrn, title, 'convert', title);
|
|
1191
1242
|
}
|
|
1192
1243
|
|
|
1244
|
+
// TEST124: Test CapGraph basic construction builds nodes and edges from caps
|
|
1193
1245
|
function testCapGraphBasicConstruction() {
|
|
1194
1246
|
console.log('Testing CapGraph: Basic construction...');
|
|
1195
1247
|
|
|
@@ -1223,6 +1275,7 @@ function testCapGraphBasicConstruction() {
|
|
|
1223
1275
|
console.log(' ✓ Basic construction');
|
|
1224
1276
|
}
|
|
1225
1277
|
|
|
1278
|
+
// TEST125: Test CapGraph getOutgoing and getIncoming return correct edges for media URN
|
|
1226
1279
|
function testCapGraphOutgoingIncoming() {
|
|
1227
1280
|
console.log('Testing CapGraph: Outgoing and incoming edges...');
|
|
1228
1281
|
|
|
@@ -1255,6 +1308,7 @@ function testCapGraphOutgoingIncoming() {
|
|
|
1255
1308
|
console.log(' ✓ Outgoing and incoming edges');
|
|
1256
1309
|
}
|
|
1257
1310
|
|
|
1311
|
+
// TEST126: Test CapGraph canConvert checks direct and transitive conversion paths
|
|
1258
1312
|
function testCapGraphCanConvert() {
|
|
1259
1313
|
console.log('Testing CapGraph: Can convert...');
|
|
1260
1314
|
|
|
@@ -1289,6 +1343,7 @@ function testCapGraphCanConvert() {
|
|
|
1289
1343
|
console.log(' ✓ Can convert');
|
|
1290
1344
|
}
|
|
1291
1345
|
|
|
1346
|
+
// TEST127: Test CapGraph findPath returns shortest path between media URNs
|
|
1292
1347
|
function testCapGraphFindPath() {
|
|
1293
1348
|
console.log('Testing CapGraph: Find path...');
|
|
1294
1349
|
|
|
@@ -1330,6 +1385,7 @@ function testCapGraphFindPath() {
|
|
|
1330
1385
|
console.log(' ✓ Find path');
|
|
1331
1386
|
}
|
|
1332
1387
|
|
|
1388
|
+
// TEST128: Test CapGraph findAllPaths returns all paths sorted by length
|
|
1333
1389
|
function testCapGraphFindAllPaths() {
|
|
1334
1390
|
console.log('Testing CapGraph: Find all paths...');
|
|
1335
1391
|
|
|
@@ -1361,6 +1417,7 @@ function testCapGraphFindAllPaths() {
|
|
|
1361
1417
|
console.log(' ✓ Find all paths');
|
|
1362
1418
|
}
|
|
1363
1419
|
|
|
1420
|
+
// TEST129: Test CapGraph getDirectEdges returns edges sorted by specificity
|
|
1364
1421
|
function testCapGraphGetDirectEdges() {
|
|
1365
1422
|
console.log('Testing CapGraph: Get direct edges...');
|
|
1366
1423
|
|
|
@@ -1397,6 +1454,7 @@ function testCapGraphGetDirectEdges() {
|
|
|
1397
1454
|
console.log(' ✓ Get direct edges');
|
|
1398
1455
|
}
|
|
1399
1456
|
|
|
1457
|
+
// TEST130: Test CapGraph stats returns node count, edge count, input/output URN counts
|
|
1400
1458
|
function testCapGraphStats() {
|
|
1401
1459
|
console.log('Testing CapGraph: Stats...');
|
|
1402
1460
|
|
|
@@ -1432,6 +1490,7 @@ function testCapGraphStats() {
|
|
|
1432
1490
|
console.log(' ✓ Stats');
|
|
1433
1491
|
}
|
|
1434
1492
|
|
|
1493
|
+
// TEST131: Test CapGraph with CapCube builds graph from multiple registries
|
|
1435
1494
|
function testCapGraphWithCapCube() {
|
|
1436
1495
|
console.log('Testing CapGraph: With CapCube...');
|
|
1437
1496
|
|
|
@@ -1473,6 +1532,7 @@ function testCapGraphWithCapCube() {
|
|
|
1473
1532
|
// StdinSource Tests
|
|
1474
1533
|
// ============================================================================
|
|
1475
1534
|
|
|
1535
|
+
// TEST156: Test creating StdinSource Data variant with byte vector
|
|
1476
1536
|
function testStdinSourceFromData() {
|
|
1477
1537
|
console.log('Testing StdinSource: From data...');
|
|
1478
1538
|
|
|
@@ -1488,13 +1548,14 @@ function testStdinSourceFromData() {
|
|
|
1488
1548
|
console.log(' ✓ From data');
|
|
1489
1549
|
}
|
|
1490
1550
|
|
|
1551
|
+
// TEST157: Test creating StdinSource FileReference variant with all required fields
|
|
1491
1552
|
function testStdinSourceFromFileReference() {
|
|
1492
1553
|
console.log('Testing StdinSource: From file reference...');
|
|
1493
1554
|
|
|
1494
1555
|
const trackedFileId = 'tracked-file-123';
|
|
1495
1556
|
const originalPath = '/path/to/original.pdf';
|
|
1496
1557
|
const securityBookmark = new Uint8Array([0x62, 0x6f, 0x6f, 0x6b]); // "book"
|
|
1497
|
-
const mediaUrn = 'media:pdf;
|
|
1558
|
+
const mediaUrn = 'media:pdf;bytes';
|
|
1498
1559
|
|
|
1499
1560
|
const source = StdinSource.fromFileReference(trackedFileId, originalPath, securityBookmark, mediaUrn);
|
|
1500
1561
|
|
|
@@ -1510,6 +1571,7 @@ function testStdinSourceFromFileReference() {
|
|
|
1510
1571
|
console.log(' ✓ From file reference');
|
|
1511
1572
|
}
|
|
1512
1573
|
|
|
1574
|
+
// TEST158: Test StdinSource Data with empty vector stores and retrieves correctly
|
|
1513
1575
|
function testStdinSourceWithEmptyData() {
|
|
1514
1576
|
console.log('Testing StdinSource: With empty data...');
|
|
1515
1577
|
|
|
@@ -1523,6 +1585,7 @@ function testStdinSourceWithEmptyData() {
|
|
|
1523
1585
|
console.log(' ✓ With empty data');
|
|
1524
1586
|
}
|
|
1525
1587
|
|
|
1588
|
+
// TEST030: Test StdinSource with null data creates valid Data source
|
|
1526
1589
|
function testStdinSourceWithNullData() {
|
|
1527
1590
|
console.log('Testing StdinSource: With null data...');
|
|
1528
1591
|
|
|
@@ -1535,6 +1598,7 @@ function testStdinSourceWithNullData() {
|
|
|
1535
1598
|
console.log(' ✓ With null data');
|
|
1536
1599
|
}
|
|
1537
1600
|
|
|
1601
|
+
// TEST159: Test StdinSource Data with binary content like PNG header bytes
|
|
1538
1602
|
function testStdinSourceWithBinaryContent() {
|
|
1539
1603
|
console.log('Testing StdinSource: With binary content...');
|
|
1540
1604
|
|
|
@@ -1551,6 +1615,7 @@ function testStdinSourceWithBinaryContent() {
|
|
|
1551
1615
|
console.log(' ✓ With binary content');
|
|
1552
1616
|
}
|
|
1553
1617
|
|
|
1618
|
+
// TEST031: Test StdinSourceKind constants are defined and distinct
|
|
1554
1619
|
function testStdinSourceKindConstants() {
|
|
1555
1620
|
console.log('Testing StdinSource: Kind constants...');
|
|
1556
1621
|
|
|
@@ -1561,6 +1626,7 @@ function testStdinSourceKindConstants() {
|
|
|
1561
1626
|
console.log(' ✓ Kind constants');
|
|
1562
1627
|
}
|
|
1563
1628
|
|
|
1629
|
+
// TEST032: Test StdinSource Data is passed correctly to executeCap
|
|
1564
1630
|
function testStdinSourcePassedToExecuteCap() {
|
|
1565
1631
|
console.log('Testing StdinSource: Passed to executeCap...');
|
|
1566
1632
|
|
|
@@ -1605,6 +1671,7 @@ function testStdinSourcePassedToExecuteCap() {
|
|
|
1605
1671
|
});
|
|
1606
1672
|
}
|
|
1607
1673
|
|
|
1674
|
+
// TEST033: Test StdinSource FileReference is passed correctly to executeCap
|
|
1608
1675
|
function testStdinSourceFileReferencePassedToExecuteCap() {
|
|
1609
1676
|
console.log('Testing StdinSource: File reference passed to executeCap...');
|
|
1610
1677
|
|
|
@@ -1636,7 +1703,7 @@ function testStdinSourceFileReferencePassedToExecuteCap() {
|
|
|
1636
1703
|
'tracked-123',
|
|
1637
1704
|
'/path/to/file.pdf',
|
|
1638
1705
|
new Uint8Array([0x42, 0x4f, 0x4f, 0x4b]),
|
|
1639
|
-
'media:pdf;
|
|
1706
|
+
'media:pdf;bytes'
|
|
1640
1707
|
);
|
|
1641
1708
|
|
|
1642
1709
|
const { compositeHost } = cube.can('cap:in="media:void";op=test;out="media:string"');
|
|
@@ -1652,11 +1719,76 @@ function testStdinSourceFileReferencePassedToExecuteCap() {
|
|
|
1652
1719
|
assert(receivedSource.isFileReference(), 'Should receive file reference source');
|
|
1653
1720
|
assertEqual(receivedSource.trackedFileId, 'tracked-123', 'Should have correct trackedFileId');
|
|
1654
1721
|
assertEqual(receivedSource.originalPath, '/path/to/file.pdf', 'Should have correct originalPath');
|
|
1655
|
-
assertEqual(receivedSource.mediaUrn, 'media:pdf;
|
|
1722
|
+
assertEqual(receivedSource.mediaUrn, 'media:pdf;bytes', 'Should have correct mediaUrn');
|
|
1656
1723
|
console.log(' ✓ File reference passed to executeCap');
|
|
1657
1724
|
});
|
|
1658
1725
|
}
|
|
1659
1726
|
|
|
1727
|
+
// ============================================================================
|
|
1728
|
+
// XV5 VALIDATION TESTS
|
|
1729
|
+
// TEST054-056: Validate that inline media_specs don't redefine registry specs
|
|
1730
|
+
// ============================================================================
|
|
1731
|
+
|
|
1732
|
+
// TEST054: Test XV5 validation detects inline media spec redefinition of built-in spec
|
|
1733
|
+
function testXV5InlineSpecRedefinitionDetected() {
|
|
1734
|
+
console.log('Testing XV5: Inline spec redefinition detected...');
|
|
1735
|
+
|
|
1736
|
+
// Try to redefine MEDIA_STRING which is a built-in spec
|
|
1737
|
+
const mediaSpecs = {
|
|
1738
|
+
[MEDIA_STRING]: {
|
|
1739
|
+
media_type: 'text/plain',
|
|
1740
|
+
title: 'My Custom String',
|
|
1741
|
+
description: 'Trying to redefine string'
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
const result = validateNoMediaSpecRedefinitionSync(mediaSpecs);
|
|
1746
|
+
|
|
1747
|
+
assert(!result.valid, 'Should fail validation when redefining built-in spec');
|
|
1748
|
+
assert(result.error && result.error.includes('XV5'), 'Error should mention XV5');
|
|
1749
|
+
assert(result.redefines && result.redefines.includes(MEDIA_STRING), 'Should identify MEDIA_STRING as redefined');
|
|
1750
|
+
|
|
1751
|
+
console.log(' ✓ Inline spec redefinition detected');
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// TEST055: Test XV5 validation allows new inline media spec not in built-ins
|
|
1755
|
+
function testXV5NewInlineSpecAllowed() {
|
|
1756
|
+
console.log('Testing XV5: New inline spec allowed...');
|
|
1757
|
+
|
|
1758
|
+
// Define a completely new media spec that doesn't exist in built-ins
|
|
1759
|
+
const mediaSpecs = {
|
|
1760
|
+
'media:my-unique-custom-type-xyz123': {
|
|
1761
|
+
media_type: 'application/json',
|
|
1762
|
+
title: 'My Custom Output',
|
|
1763
|
+
description: 'A custom output type'
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
|
|
1767
|
+
const result = validateNoMediaSpecRedefinitionSync(mediaSpecs);
|
|
1768
|
+
|
|
1769
|
+
assert(result.valid, 'Should pass validation for new spec not in built-ins');
|
|
1770
|
+
assert(!result.error, 'Should not have error message');
|
|
1771
|
+
|
|
1772
|
+
console.log(' ✓ New inline spec allowed');
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// TEST056: Test XV5 validation passes for empty or null media_specs
|
|
1776
|
+
function testXV5EmptyMediaSpecsAllowed() {
|
|
1777
|
+
console.log('Testing XV5: Empty media_specs allowed...');
|
|
1778
|
+
|
|
1779
|
+
// Empty or null media_specs should pass
|
|
1780
|
+
let result = validateNoMediaSpecRedefinitionSync({});
|
|
1781
|
+
assert(result.valid, 'Empty object should pass validation');
|
|
1782
|
+
|
|
1783
|
+
result = validateNoMediaSpecRedefinitionSync(null);
|
|
1784
|
+
assert(result.valid, 'Null should pass validation');
|
|
1785
|
+
|
|
1786
|
+
result = validateNoMediaSpecRedefinitionSync(undefined);
|
|
1787
|
+
assert(result.valid, 'Undefined should pass validation');
|
|
1788
|
+
|
|
1789
|
+
console.log(' ✓ Empty media_specs allowed');
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1660
1792
|
// Update runTests to include new tests
|
|
1661
1793
|
async function runTests() {
|
|
1662
1794
|
console.log('Running Cap URN JavaScript tests...\n');
|
|
@@ -1737,6 +1869,11 @@ async function runTests() {
|
|
|
1737
1869
|
await testStdinSourcePassedToExecuteCap();
|
|
1738
1870
|
await testStdinSourceFileReferencePassedToExecuteCap();
|
|
1739
1871
|
|
|
1872
|
+
// XV5 validation tests
|
|
1873
|
+
testXV5InlineSpecRedefinitionDetected();
|
|
1874
|
+
testXV5NewInlineSpecAllowed();
|
|
1875
|
+
testXV5EmptyMediaSpecsAllowed();
|
|
1876
|
+
|
|
1740
1877
|
console.log('OK All tests passed!');
|
|
1741
1878
|
}
|
|
1742
1879
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Bahram Joharshamshiri",
|
|
3
3
|
"dependencies": {
|
|
4
|
-
"tagged-urn": "^0.
|
|
4
|
+
"tagged-urn": "^0.18.2934"
|
|
5
5
|
},
|
|
6
6
|
"description": "JavaScript implementation of Cap URN (Capability Uniform Resource Names) with strict validation and matching",
|
|
7
7
|
"engines": {
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"scripts": {
|
|
33
33
|
"test": "node capns.test.js"
|
|
34
34
|
},
|
|
35
|
-
"version": "0.
|
|
35
|
+
"version": "0.58.11575"
|
|
36
36
|
}
|