capns 0.32.9247

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.
Files changed (5) hide show
  1. package/README.md +130 -0
  2. package/RULES.md +55 -0
  3. package/capns.js +2834 -0
  4. package/capns.test.js +1459 -0
  5. package/package.json +36 -0
package/capns.test.js ADDED
@@ -0,0 +1,1459 @@
1
+ // Cap URN JavaScript Test Suite
2
+ // Tests all the same rules as Rust, Go, and Objective-C implementations
3
+
4
+ const {
5
+ CapUrn,
6
+ CapUrnBuilder,
7
+ CapMatcher,
8
+ CapUrnError,
9
+ ErrorCodes,
10
+ Cap,
11
+ MediaSpec,
12
+ MediaSpecError,
13
+ MediaSpecErrorCodes,
14
+ resolveMediaUrn,
15
+ isBuiltinMediaUrn,
16
+ BUILTIN_SPECS,
17
+ MEDIA_STRING,
18
+ MEDIA_INTEGER,
19
+ MEDIA_NUMBER,
20
+ MEDIA_BOOLEAN,
21
+ MEDIA_OBJECT,
22
+ MEDIA_BINARY,
23
+ MEDIA_VOID,
24
+ // CapMatrix and CapCube
25
+ CapMatrixError,
26
+ CapMatrix,
27
+ BestCapSetMatch,
28
+ CompositeCapSet,
29
+ CapCube,
30
+ // CapGraph
31
+ CapGraphEdge,
32
+ CapGraphStats,
33
+ CapGraph
34
+ } = require('./capns.js');
35
+
36
+ // Test assertion utility
37
+ function assert(condition, message) {
38
+ if (!condition) {
39
+ throw new Error(`Assertion failed: ${message}`);
40
+ }
41
+ }
42
+
43
+ function assertEqual(actual, expected, message) {
44
+ if (actual !== expected) {
45
+ throw new Error(`Assertion failed: ${message}. Expected: ${expected}, Actual: ${actual}`);
46
+ }
47
+ }
48
+
49
+ function assertThrows(fn, expectedErrorCode, message) {
50
+ try {
51
+ fn();
52
+ throw new Error(`Expected error but function succeeded: ${message}`);
53
+ } catch (error) {
54
+ if (error instanceof CapUrnError && error.code === expectedErrorCode) {
55
+ return; // Expected error
56
+ }
57
+ throw new Error(`Expected CapUrnError with code ${expectedErrorCode} but got: ${error.message}`);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Helper function to build test URNs with required in/out media URNs
63
+ * @param {string} tags - Additional tags to add (empty string for minimal URN)
64
+ * @returns {string} A valid cap URN string with in/out
65
+ */
66
+ function testUrn(tags) {
67
+ if (!tags || tags === '') {
68
+ return 'cap:in="media:type=void;v=1";out="media:type=object;v=1"';
69
+ }
70
+ return 'cap:in="media:type=void;v=1";out="media:type=object;v=1";' + tags;
71
+ }
72
+
73
+ // Test suite - defined at the end of file
74
+
75
+ function testCapUrnCreation() {
76
+ console.log('Testing Cap URN creation...');
77
+
78
+ const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf;target=thumbnail'));
79
+ assertEqual(cap.getTag('op'), 'generate', 'Should get action tag');
80
+ assertEqual(cap.getTag('target'), 'thumbnail', 'Should get target tag');
81
+ assertEqual(cap.getTag('ext'), 'pdf', 'Should get ext tag');
82
+ assertEqual(cap.getInSpec(), 'media:type=void;v=1', 'Should get inSpec');
83
+ assertEqual(cap.getOutSpec(), 'media:type=object;v=1', 'Should get outSpec');
84
+
85
+ console.log(' ✓ Cap URN creation');
86
+ }
87
+
88
+ function testCaseInsensitive() {
89
+ console.log('Testing case insensitive behavior...');
90
+
91
+ // Test that different casing produces the same URN
92
+ const cap1 = CapUrn.fromString('cap:IN="media:type=void;v=1";OUT="media:type=object;v=1";OP=Generate;EXT=PDF;Target=Thumbnail');
93
+ const cap2 = CapUrn.fromString(testUrn('op=generate;ext=pdf;target=thumbnail'));
94
+
95
+ // Both should be normalized to lowercase
96
+ assertEqual(cap1.getTag('op'), 'generate', 'Should normalize op to lowercase');
97
+ assertEqual(cap1.getTag('ext'), 'pdf', 'Should normalize ext to lowercase');
98
+ assertEqual(cap1.getTag('target'), 'thumbnail', 'Should normalize target to lowercase');
99
+
100
+ // URNs should be identical after normalization
101
+ assertEqual(cap1.toString(), cap2.toString(), 'URNs should be equal after normalization');
102
+
103
+ // PartialEq should work correctly - URNs with different case should be equal
104
+ assert(cap1.equals(cap2), 'URNs with different case should be equal');
105
+
106
+ // Case-insensitive tag lookup should work
107
+ assertEqual(cap1.getTag('OP'), 'generate', 'Should lookup with case-insensitive key');
108
+ assertEqual(cap1.getTag('Op'), 'generate', 'Should lookup with mixed case key');
109
+ assert(cap1.hasTag('op', 'generate'), 'Should match with case-insensitive comparison');
110
+ assert(cap1.hasTag('OP', 'generate'), 'Should match with case-insensitive comparison');
111
+
112
+ // Case-insensitive in/out lookup
113
+ assertEqual(cap1.getTag('IN'), 'media:type=void;v=1', 'Should lookup in with case-insensitive key');
114
+ assertEqual(cap1.getTag('OUT'), 'media:type=object;v=1', 'Should lookup out with case-insensitive key');
115
+
116
+ // Matching should work case-insensitively
117
+ assert(cap1.matches(cap2), 'Should match case-insensitively');
118
+ assert(cap2.matches(cap1), 'Should match case-insensitively');
119
+
120
+ console.log(' ✓ Case insensitive behavior');
121
+ }
122
+
123
+ function testCapPrefixRequired() {
124
+ console.log('Testing cap: prefix requirement...');
125
+
126
+ // Missing cap: prefix should fail
127
+ assertThrows(
128
+ () => CapUrn.fromString('in="media:type=void;v=1";out="media:type=object;v=1";op=generate'),
129
+ ErrorCodes.MISSING_CAP_PREFIX,
130
+ 'Should require cap: prefix'
131
+ );
132
+
133
+ // Valid cap: prefix should work
134
+ const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
135
+ assertEqual(cap.getTag('op'), 'generate', 'Should parse with valid cap: prefix');
136
+
137
+ console.log(' ✓ Cap prefix requirement');
138
+ }
139
+
140
+ function testTrailingSemicolonEquivalence() {
141
+ console.log('Testing trailing semicolon equivalence...');
142
+
143
+ // Both with and without trailing semicolon should be equivalent
144
+ const cap1 = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
145
+ const cap2 = CapUrn.fromString(testUrn('op=generate;ext=pdf') + ';');
146
+
147
+ // They should be equal
148
+ assert(cap1.equals(cap2), 'Should be equal with/without trailing semicolon');
149
+
150
+ // They should have same string representation (canonical form)
151
+ assertEqual(cap1.toString(), cap2.toString(), 'Should have same canonical form');
152
+
153
+ // They should match each other
154
+ assert(cap1.matches(cap2), 'Should match each other');
155
+ assert(cap2.matches(cap1), 'Should match each other');
156
+
157
+ console.log(' ✓ Trailing semicolon equivalence');
158
+ }
159
+
160
+ function testCanonicalStringFormat() {
161
+ console.log('Testing canonical string format...');
162
+
163
+ const cap = CapUrn.fromString(testUrn('op=generate;target=thumbnail;ext=pdf'));
164
+ // Should be sorted alphabetically and have no trailing semicolon in canonical form
165
+ // in/out are included in alphabetical order: 'ext' < 'in' < 'op' < 'out' < 'target'
166
+ assertEqual(cap.toString(), 'cap:ext=pdf;in="media:type=void;v=1";op=generate;out="media:type=object;v=1";target=thumbnail', 'Should be alphabetically sorted');
167
+
168
+ console.log(' ✓ Canonical string format');
169
+ }
170
+
171
+ function testTagMatching() {
172
+ console.log('Testing tag matching...');
173
+
174
+ const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf;target=thumbnail'));
175
+
176
+ // Exact match
177
+ const request1 = CapUrn.fromString(testUrn('op=generate;ext=pdf;target=thumbnail'));
178
+ assert(cap.matches(request1), 'Should match exact request');
179
+
180
+ // Subset match (other tags)
181
+ const request2 = CapUrn.fromString(testUrn('op=generate'));
182
+ assert(cap.matches(request2), 'Should match subset request');
183
+
184
+ // Wildcard request should match specific cap
185
+ const request3 = CapUrn.fromString(testUrn('ext=*'));
186
+ assert(cap.matches(request3), 'Should match wildcard request');
187
+
188
+ // No match - conflicting value
189
+ const request4 = CapUrn.fromString(testUrn('op=extract'));
190
+ assert(!cap.matches(request4), 'Should not match conflicting value');
191
+
192
+ // Direction must match
193
+ const request5 = CapUrn.fromString('cap:in="media:type=string;v=1";out="media:type=object;v=1";op=generate');
194
+ assert(!cap.matches(request5), 'Should not match different inSpec');
195
+
196
+ console.log(' ✓ Tag matching');
197
+ }
198
+
199
+ function testMissingTagHandling() {
200
+ console.log('Testing missing tag handling...');
201
+
202
+ const cap = CapUrn.fromString(testUrn('op=generate'));
203
+
204
+ // Request with tag should match cap without tag (treated as wildcard)
205
+ const request1 = CapUrn.fromString(testUrn('ext=pdf'));
206
+ assert(cap.matches(request1), 'Should match when cap has missing tag (wildcard)');
207
+
208
+ // But cap with extra tags can match subset requests
209
+ const cap2 = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
210
+ const request2 = CapUrn.fromString(testUrn('op=generate'));
211
+ assert(cap2.matches(request2), 'Should match subset request');
212
+
213
+ console.log(' ✓ Missing tag handling');
214
+ }
215
+
216
+ function testSpecificity() {
217
+ console.log('Testing specificity...');
218
+
219
+ // Specificity now includes in/out (2) plus other tags
220
+ const cap1 = CapUrn.fromString(testUrn('type=general'));
221
+ const cap2 = CapUrn.fromString(testUrn('op=generate'));
222
+ const cap3 = CapUrn.fromString(testUrn('op=*;ext=pdf'));
223
+
224
+ // Base: in=media:type=void;v=1 (1) + out=media:type=object;v=1 (1) = 2
225
+ assertEqual(cap1.specificity(), 3, 'Should have specificity 3 (2 for in/out + 1 for type)');
226
+ assertEqual(cap2.specificity(), 3, 'Should have specificity 3 (2 for in/out + 1 for op)');
227
+ assertEqual(cap3.specificity(), 3, 'Should have specificity 3 (2 for in/out + 1 for ext, op=* does not count)');
228
+
229
+ // Test with wildcard in/out
230
+ const cap4 = CapUrn.fromString('cap:in=*;out=*;op=generate');
231
+ assertEqual(cap4.specificity(), 1, 'Should have specificity 1 (wildcards for in/out do not count)');
232
+
233
+ assert(!cap2.isMoreSpecificThan(cap1), 'Different tags should not be more specific');
234
+
235
+ console.log(' ✓ Specificity');
236
+ }
237
+
238
+ function testCompatibility() {
239
+ console.log('Testing compatibility...');
240
+
241
+ const cap1 = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
242
+ const cap2 = CapUrn.fromString(testUrn('op=generate;format=*'));
243
+ const cap3 = CapUrn.fromString(testUrn('type=image;op=extract'));
244
+
245
+ assert(cap1.isCompatibleWith(cap2), 'Should be compatible');
246
+ assert(cap2.isCompatibleWith(cap1), 'Should be compatible');
247
+ assert(!cap1.isCompatibleWith(cap3), 'Should not be compatible (different op)');
248
+
249
+ // Missing tags are treated as wildcards for compatibility
250
+ const cap4 = CapUrn.fromString(testUrn('op=generate'));
251
+ assert(cap1.isCompatibleWith(cap4), 'Should be compatible with missing tags');
252
+ assert(cap4.isCompatibleWith(cap1), 'Should be compatible with missing tags');
253
+
254
+ // Different in/out should not be compatible
255
+ const cap5 = CapUrn.fromString('cap:in="media:type=string;v=1";out="media:type=object;v=1";op=generate');
256
+ assert(!cap1.isCompatibleWith(cap5), 'Should not be compatible with different inSpec');
257
+
258
+ console.log(' ✓ Compatibility');
259
+ }
260
+
261
+ function testBuilder() {
262
+ console.log('Testing builder...');
263
+
264
+ const cap = new CapUrnBuilder()
265
+ .inSpec('media:type=void;v=1')
266
+ .outSpec('media:type=object;v=1')
267
+ .tag('op', 'generate')
268
+ .tag('target', 'thumbnail')
269
+ .tag('ext', 'pdf')
270
+ .tag('format', 'binary')
271
+ .build();
272
+
273
+ assertEqual(cap.getTag('op'), 'generate', 'Should build with op tag');
274
+ assertEqual(cap.getTag('format'), 'binary', 'Should build with format tag');
275
+ assertEqual(cap.getInSpec(), 'media:type=void;v=1', 'Should build with inSpec');
276
+ assertEqual(cap.getOutSpec(), 'media:type=object;v=1', 'Should build with outSpec');
277
+
278
+ // Builder should require inSpec and outSpec
279
+ assertThrows(
280
+ () => new CapUrnBuilder().tag('op', 'test').build(),
281
+ ErrorCodes.MISSING_IN_SPEC,
282
+ 'Should require inSpec'
283
+ );
284
+
285
+ assertThrows(
286
+ () => new CapUrnBuilder().inSpec('media:type=void;v=1').tag('op', 'test').build(),
287
+ ErrorCodes.MISSING_OUT_SPEC,
288
+ 'Should require outSpec'
289
+ );
290
+
291
+ console.log(' ✓ Builder');
292
+ }
293
+
294
+ function testConvenienceMethods() {
295
+ console.log('Testing convenience methods...');
296
+
297
+ const original = CapUrn.fromString(testUrn('op=generate'));
298
+
299
+ // Test withTag
300
+ const modified = original.withTag('ext', 'pdf');
301
+ assertEqual(modified.getTag('op'), 'generate', 'Should preserve original tag');
302
+ assertEqual(modified.getTag('ext'), 'pdf', 'Should add new tag');
303
+ assertEqual(modified.getInSpec(), 'media:type=void;v=1', 'Should preserve inSpec');
304
+ assertEqual(modified.getOutSpec(), 'media:type=object;v=1', 'Should preserve outSpec');
305
+
306
+ // Test withTag silently ignores in/out
307
+ const modified2 = original.withTag('in', 'media:type=string;v=1');
308
+ assertEqual(modified2.getInSpec(), 'media:type=void;v=1', 'withTag should ignore in');
309
+ assert(modified2 === original, 'withTag(in) should return same object');
310
+
311
+ // Test withInSpec/withOutSpec
312
+ const modifiedIn = original.withInSpec('media:type=string;v=1');
313
+ assertEqual(modifiedIn.getInSpec(), 'media:type=string;v=1', 'withInSpec should change inSpec');
314
+ const modifiedOut = original.withOutSpec('media:type=binary;v=1');
315
+ assertEqual(modifiedOut.getOutSpec(), 'media:type=binary;v=1', 'withOutSpec should change outSpec');
316
+
317
+ // Test withoutTag
318
+ const removed = modified.withoutTag('op');
319
+ assertEqual(removed.getTag('ext'), 'pdf', 'Should preserve remaining tag');
320
+ assertEqual(removed.getTag('op'), undefined, 'Should remove specified tag');
321
+ assertEqual(removed.getInSpec(), 'media:type=void;v=1', 'Should preserve inSpec after withoutTag');
322
+
323
+ // Test withoutTag silently ignores in/out
324
+ const removed2 = modified.withoutTag('in');
325
+ assertEqual(removed2.getInSpec(), 'media:type=void;v=1', 'withoutTag should ignore in');
326
+ assert(removed2 === modified, 'withoutTag(in) should return same object');
327
+
328
+ // Test merge (direction from other)
329
+ const cap1 = CapUrn.fromString(testUrn('op=generate'));
330
+ const cap2 = CapUrn.fromString('cap:in="media:type=string;v=1";out="media:type=binary;v=1";ext=pdf;format=binary');
331
+ const merged = cap1.merge(cap2);
332
+ assertEqual(merged.getInSpec(), 'media:type=string;v=1', 'merge should take inSpec from other');
333
+ assertEqual(merged.getOutSpec(), 'media:type=binary;v=1', 'merge should take outSpec from other');
334
+ assertEqual(merged.getTag('op'), 'generate', 'merge should keep original tags');
335
+ assertEqual(merged.getTag('ext'), 'pdf', 'merge should add other tags');
336
+
337
+ // Test subset (always preserves in/out)
338
+ const subset = merged.subset(['type', 'ext']);
339
+ assertEqual(subset.getTag('ext'), 'pdf', 'Should include ext');
340
+ assertEqual(subset.getTag('op'), undefined, 'Should not include op');
341
+ assertEqual(subset.getInSpec(), 'media:type=string;v=1', 'subset should preserve inSpec');
342
+ assertEqual(subset.getOutSpec(), 'media:type=binary;v=1', 'subset should preserve outSpec');
343
+
344
+ // Test wildcardTag for in/out
345
+ const cap = CapUrn.fromString(testUrn('ext=pdf'));
346
+ const wildcardedExt = cap.withWildcardTag('ext');
347
+ assertEqual(wildcardedExt.getTag('ext'), '*', 'Should set ext wildcard');
348
+ const wildcardedIn = cap.withWildcardTag('in');
349
+ assertEqual(wildcardedIn.getInSpec(), '*', 'Should set in wildcard');
350
+ const wildcardedOut = cap.withWildcardTag('out');
351
+ assertEqual(wildcardedOut.getOutSpec(), '*', 'Should set out wildcard');
352
+
353
+ console.log(' ✓ Convenience methods');
354
+ }
355
+
356
+ function testCapMatcher() {
357
+ console.log('Testing CapMatcher...');
358
+
359
+ const caps = [
360
+ CapUrn.fromString('cap:in=*;out=*;op=*'),
361
+ CapUrn.fromString(testUrn('op=generate')),
362
+ CapUrn.fromString(testUrn('op=generate;ext=pdf'))
363
+ ];
364
+
365
+ const request = CapUrn.fromString(testUrn('op=generate'));
366
+ const best = CapMatcher.findBestMatch(caps, request);
367
+
368
+ // Most specific cap that can handle the request (ext=pdf is more specific)
369
+ // Canonical order is alphabetical: ext, in, op, out
370
+ assertEqual(best.toString(), 'cap:ext=pdf;in="media:type=void;v=1";op=generate;out="media:type=object;v=1"', 'Should find most specific match');
371
+
372
+ // Test findAllMatches - now only 2 match because first has wildcard in/out
373
+ const matches = CapMatcher.findAllMatches(caps, request);
374
+ assertEqual(matches.length, 3, 'Should find all matches (wildcard in/out matches any)');
375
+ // First should be most specific (ext=pdf;op=generate with in/out)
376
+ assertEqual(matches[0].getTag('ext'), 'pdf', 'Most specific should have ext=pdf');
377
+
378
+ console.log(' ✓ CapMatcher');
379
+ }
380
+
381
+ function testJSONSerialization() {
382
+ console.log('Testing JSON serialization...');
383
+
384
+ const original = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
385
+ const json = JSON.stringify({ urn: original.toString() });
386
+ const parsed = JSON.parse(json);
387
+ const restored = CapUrn.fromString(parsed.urn);
388
+
389
+ assert(original.equals(restored), 'Should serialize/deserialize correctly');
390
+ assertEqual(restored.getInSpec(), 'media:type=void;v=1', 'Should preserve inSpec');
391
+ assertEqual(restored.getOutSpec(), 'media:type=object;v=1', 'Should preserve outSpec');
392
+
393
+ console.log(' ✓ JSON serialization');
394
+ }
395
+
396
+ function testEmptyCapUrn() {
397
+ console.log('Testing empty cap URN now fails (in/out required)...');
398
+
399
+ // Empty cap URN now FAILS because in/out are required
400
+ assertThrows(
401
+ () => CapUrn.fromString('cap:'),
402
+ ErrorCodes.MISSING_IN_SPEC,
403
+ 'Empty cap URN should fail with MISSING_IN_SPEC'
404
+ );
405
+
406
+ // Missing out should fail
407
+ assertThrows(
408
+ () => CapUrn.fromString('cap:in="media:type=void;v=1"'),
409
+ ErrorCodes.MISSING_OUT_SPEC,
410
+ 'Cap URN without out should fail with MISSING_OUT_SPEC'
411
+ );
412
+
413
+ // Minimal valid cap (just in/out)
414
+ const minimal = CapUrn.fromString(testUrn(''));
415
+ assertEqual(Object.keys(minimal.tags).length, 0, 'Should have no other tags');
416
+ assertEqual(minimal.getInSpec(), 'media:type=void;v=1', 'Should have inSpec');
417
+ assertEqual(minimal.getOutSpec(), 'media:type=object;v=1', 'Should have outSpec');
418
+
419
+ // For "match anything" behavior, use wildcards
420
+ const wildcard = CapUrn.fromString('cap:in=*;out=*');
421
+ const specific = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
422
+ assert(wildcard.matches(specific), 'Wildcard should match any cap');
423
+ assert(wildcard.matches(wildcard), 'Wildcard should match itself');
424
+
425
+ console.log(' ✓ Empty cap URN now fails (in/out required)');
426
+ }
427
+
428
+ function testExtendedCharacterSupport() {
429
+ console.log('Testing extended character support...');
430
+
431
+ // Test forward slashes and colons in tag components
432
+ const cap = CapUrn.fromString(testUrn('url=https://example_org/api;path=/some/file'));
433
+ assertEqual(cap.getTag('url'), 'https://example_org/api', 'Should support colons and slashes');
434
+ assertEqual(cap.getTag('path'), '/some/file', 'Should support slashes');
435
+
436
+ console.log(' ✓ Extended character support');
437
+ }
438
+
439
+ function testWildcardRestrictions() {
440
+ console.log('Testing wildcard restrictions...');
441
+
442
+ // Wildcard should be rejected in keys
443
+ assertThrows(
444
+ () => CapUrn.fromString(testUrn('*=value')),
445
+ ErrorCodes.INVALID_CHARACTER,
446
+ 'Should reject wildcard in key'
447
+ );
448
+
449
+ // Wildcard should be accepted in values
450
+ const cap = CapUrn.fromString(testUrn('key=*'));
451
+ assertEqual(cap.getTag('key'), '*', 'Should accept wildcard in value');
452
+
453
+ // Wildcard in in/out should work
454
+ const capWild = CapUrn.fromString('cap:in=*;out=*;key=value');
455
+ assertEqual(capWild.getInSpec(), '*', 'Should accept wildcard in inSpec');
456
+ assertEqual(capWild.getOutSpec(), '*', 'Should accept wildcard in outSpec');
457
+
458
+ console.log(' ✓ Wildcard restrictions');
459
+ }
460
+
461
+ function testDuplicateKeyRejection() {
462
+ console.log('Testing duplicate key rejection...');
463
+
464
+ // Duplicate keys should be rejected
465
+ assertThrows(
466
+ () => CapUrn.fromString(testUrn('key=value1;key=value2')),
467
+ ErrorCodes.DUPLICATE_KEY,
468
+ 'Should reject duplicate keys'
469
+ );
470
+
471
+ console.log(' ✓ Duplicate key rejection');
472
+ }
473
+
474
+ function testNumericKeyRestriction() {
475
+ console.log('Testing numeric key restriction...');
476
+
477
+ // Pure numeric keys should be rejected
478
+ assertThrows(
479
+ () => CapUrn.fromString(testUrn('123=value')),
480
+ ErrorCodes.NUMERIC_KEY,
481
+ 'Should reject numeric keys'
482
+ );
483
+
484
+ // Mixed alphanumeric keys should be allowed
485
+ const mixedKey1 = CapUrn.fromString(testUrn('key123=value'));
486
+ assertEqual(mixedKey1.getTag('key123'), 'value', 'Should allow mixed alphanumeric keys');
487
+
488
+ const mixedKey2 = CapUrn.fromString(testUrn('x123key=value'));
489
+ assertEqual(mixedKey2.getTag('x123key'), 'value', 'Should allow mixed alphanumeric keys');
490
+
491
+ // Pure numeric values should be allowed
492
+ const numericValue = CapUrn.fromString(testUrn('key=123'));
493
+ assertEqual(numericValue.getTag('key'), '123', 'Should allow numeric values');
494
+
495
+ console.log(' ✓ Numeric key restriction');
496
+ }
497
+
498
+ // ============================================================================
499
+ // NEW FORMAT TESTS - Spec ID Resolution and MediaSpec
500
+ // ============================================================================
501
+
502
+ function testMediaSpecCanonicalFormat() {
503
+ console.log('Testing MediaSpec canonical format...');
504
+
505
+ // Should parse canonical format (no content-type: prefix)
506
+ const spec1 = MediaSpec.parse('text/plain; profile=https://capns.org/schema/str');
507
+ assertEqual(spec1.contentType, 'text/plain', 'Should parse content type');
508
+ assertEqual(spec1.profile, 'https://capns.org/schema/str', 'Should parse profile');
509
+
510
+ // Should parse without profile
511
+ const spec2 = MediaSpec.parse('application/json');
512
+ assertEqual(spec2.contentType, 'application/json', 'Should parse content type without profile');
513
+ assertEqual(spec2.profile, null, 'Should have null profile');
514
+
515
+ // Should output canonical form (no prefix)
516
+ assertEqual(spec1.toString(), 'text/plain; profile="https://capns.org/schema/str"', 'Should output canonical form');
517
+ assertEqual(spec2.toString(), 'application/json', 'Should output content type only');
518
+
519
+ console.log(' ✓ MediaSpec canonical format');
520
+ }
521
+
522
+ function testMediaSpecLegacyFormatRejection() {
523
+ console.log('Testing legacy format rejection...');
524
+
525
+ // MUST FAIL HARD on legacy content-type: prefix
526
+ let caught = false;
527
+ try {
528
+ MediaSpec.parse('content-type: text/plain; profile=https://example.com');
529
+ } catch (e) {
530
+ if (e instanceof MediaSpecError && e.code === MediaSpecErrorCodes.LEGACY_FORMAT) {
531
+ caught = true;
532
+ } else {
533
+ throw new Error(`Expected MediaSpecError with LEGACY_FORMAT code, got: ${e.message}`);
534
+ }
535
+ }
536
+ assert(caught, 'Should reject legacy content-type: prefix');
537
+
538
+ console.log(' ✓ Legacy format rejection');
539
+ }
540
+
541
+ function testBuiltinSpecIds() {
542
+ console.log('Testing built-in spec IDs...');
543
+
544
+ // Verify built-in spec IDs exist
545
+ assert(isBuiltinMediaUrn(MEDIA_STRING), 'MEDIA_STRING should be built-in');
546
+ assert(isBuiltinMediaUrn(MEDIA_INTEGER), 'MEDIA_INTEGER should be built-in');
547
+ assert(isBuiltinMediaUrn(MEDIA_NUMBER), 'MEDIA_NUMBER should be built-in');
548
+ assert(isBuiltinMediaUrn(MEDIA_BOOLEAN), 'MEDIA_BOOLEAN should be built-in');
549
+ assert(isBuiltinMediaUrn(MEDIA_OBJECT), 'MEDIA_OBJECT should be built-in');
550
+ assert(isBuiltinMediaUrn(MEDIA_BINARY), 'MEDIA_BINARY should be built-in');
551
+
552
+ // Non-existent spec should not be built-in
553
+ assert(!isBuiltinMediaUrn('media:type=nonexistent;v=1'), 'Non-existent spec should not be built-in');
554
+
555
+ console.log(' ✓ Built-in spec IDs');
556
+ }
557
+
558
+ function testSpecIdResolution() {
559
+ console.log('Testing spec ID resolution...');
560
+
561
+ // Should resolve built-in spec IDs
562
+ const strSpec = resolveMediaUrn(MEDIA_STRING);
563
+ assertEqual(strSpec.contentType, 'text/plain', 'Should resolve str spec');
564
+ assertEqual(strSpec.profile, 'https://capns.org/schema/str', 'Should have correct profile');
565
+
566
+ const intSpec = resolveMediaUrn(MEDIA_INTEGER);
567
+ assertEqual(intSpec.contentType, 'text/plain', 'Should resolve int spec');
568
+ assertEqual(intSpec.profile, 'https://capns.org/schema/int', 'Should have correct profile');
569
+
570
+ const objSpec = resolveMediaUrn(MEDIA_OBJECT);
571
+ assertEqual(objSpec.contentType, 'application/json', 'Should resolve obj spec');
572
+
573
+ const binarySpec = resolveMediaUrn(MEDIA_BINARY);
574
+ assertEqual(binarySpec.contentType, 'application/octet-stream', 'Should resolve binary spec');
575
+ assert(binarySpec.isBinary(), 'Binary spec should report isBinary()');
576
+
577
+ console.log(' ✓ Spec ID resolution');
578
+ }
579
+
580
+ function testMediaUrnResolutionWithMediaSpecs() {
581
+ console.log('Testing media URN resolution with custom mediaSpecs...');
582
+
583
+ // Custom mediaSpecs table (using media URN format as keys)
584
+ const mediaSpecs = {
585
+ 'media:type=custom-json;v=1': 'application/json; profile=https://example.com/schema/custom',
586
+ 'media:type=rich-xml;v=1': {
587
+ media_type: 'application/xml',
588
+ profile_uri: 'https://example.com/schema/rich',
589
+ schema: { type: 'object' }
590
+ }
591
+ };
592
+
593
+ // Should resolve custom string form
594
+ const customSpec = resolveMediaUrn('media:type=custom-json;v=1', mediaSpecs);
595
+ assertEqual(customSpec.contentType, 'application/json', 'Should resolve custom spec');
596
+ assertEqual(customSpec.profile, 'https://example.com/schema/custom', 'Should have custom profile');
597
+
598
+ // Should resolve custom object form with schema
599
+ const richSpec = resolveMediaUrn('media:type=rich-xml;v=1', mediaSpecs);
600
+ assertEqual(richSpec.contentType, 'application/xml', 'Should resolve rich spec');
601
+ assertEqual(richSpec.profile, 'https://example.com/schema/rich', 'Should have rich profile');
602
+ assert(richSpec.schema !== null, 'Should have schema from object form');
603
+
604
+ // Should still resolve built-ins when not in custom table
605
+ const strSpec = resolveMediaUrn(MEDIA_STRING, mediaSpecs);
606
+ assertEqual(strSpec.contentType, 'text/plain', 'Should still resolve built-in');
607
+
608
+ console.log(' ✓ Media URN resolution with custom mediaSpecs');
609
+ }
610
+
611
+ function testMediaUrnResolutionFailHard() {
612
+ console.log('Testing media URN resolution fail hard...');
613
+
614
+ // Should FAIL HARD on unresolvable media URN
615
+ let caught = false;
616
+ try {
617
+ resolveMediaUrn('media:type=nonexistent;v=1', {});
618
+ } catch (e) {
619
+ if (e instanceof MediaSpecError && e.code === MediaSpecErrorCodes.UNRESOLVABLE_MEDIA_URN) {
620
+ caught = true;
621
+ } else {
622
+ throw new Error(`Expected MediaSpecError with UNRESOLVABLE_MEDIA_URN code, got: ${e.message}`);
623
+ }
624
+ }
625
+ assert(caught, 'Should fail hard on unresolvable media URN');
626
+
627
+ console.log(' ✓ Media URN resolution fail hard');
628
+ }
629
+
630
+ function testCapWithMediaSpecs() {
631
+ console.log('Testing Cap with mediaSpecs...');
632
+
633
+ // Now in/out are parsed as first-class fields with media URNs
634
+ const urn = CapUrn.fromString('cap:in="media:type=string;v=1";op=test;out="media:type=custom;v=1"');
635
+ assertEqual(urn.getInSpec(), 'media:type=string;v=1', 'Should parse inSpec');
636
+ assertEqual(urn.getOutSpec(), 'media:type=custom;v=1', 'Should parse outSpec');
637
+
638
+ const cap = new Cap(urn, 'Test Cap', 'test_command');
639
+
640
+ // Set custom mediaSpecs
641
+ cap.mediaSpecs = {
642
+ 'media:type=custom;v=1': {
643
+ media_type: 'application/json',
644
+ profile_uri: 'https://example.com/schema/output',
645
+ schema: {
646
+ type: 'object',
647
+ properties: { result: { type: 'string' } }
648
+ }
649
+ }
650
+ };
651
+
652
+ // Should resolve built-in via cap.resolveMediaUrn
653
+ const strSpec = cap.resolveMediaUrn(MEDIA_STRING);
654
+ assertEqual(strSpec.contentType, 'text/plain', 'Should resolve built-in through cap');
655
+
656
+ // Should resolve custom spec via cap.resolveMediaUrn
657
+ const outputSpec = cap.resolveMediaUrn('media:type=custom;v=1');
658
+ assertEqual(outputSpec.contentType, 'application/json', 'Should resolve custom spec through cap');
659
+ assert(outputSpec.schema !== null, 'Should have schema');
660
+
661
+ console.log(' ✓ Cap with mediaSpecs');
662
+ }
663
+
664
+ function testCapJSONSerialization() {
665
+ console.log('Testing Cap JSON serialization with mediaSpecs...');
666
+
667
+ const urn = CapUrn.fromString(testUrn('op=test'));
668
+ const cap = new Cap(urn, 'Test Cap', 'test_command');
669
+ cap.mediaSpecs = {
670
+ 'media:type=custom;v=1': 'text/plain; profile=https://example.com'
671
+ };
672
+ cap.arguments = {
673
+ required: [{ name: 'input', media_urn: MEDIA_STRING }],
674
+ optional: []
675
+ };
676
+ cap.output = { media_urn: 'media:type=custom;v=1', output_description: 'Test output' };
677
+
678
+ // Serialize to JSON
679
+ const json = cap.toJSON();
680
+ assert(json.media_specs !== undefined, 'Should have media_specs in JSON');
681
+ assertEqual(json.media_specs['media:type=custom;v=1'], 'text/plain; profile=https://example.com', 'Should serialize mediaSpecs');
682
+ // URN tags should include in and out
683
+ assertEqual(json.urn.tags['in'], 'media:type=void;v=1', 'Should serialize inSpec in tags');
684
+ assertEqual(json.urn.tags['out'], 'media:type=object;v=1', 'Should serialize outSpec in tags');
685
+
686
+ // Deserialize from JSON
687
+ const restored = Cap.fromJSON(json);
688
+ assert(restored.mediaSpecs !== undefined, 'Should restore mediaSpecs');
689
+ assertEqual(restored.mediaSpecs['media:type=custom;v=1'], 'text/plain; profile=https://example.com', 'Should restore mediaSpecs content');
690
+ assertEqual(restored.urn.getInSpec(), 'media:type=void;v=1', 'Should restore inSpec');
691
+ assertEqual(restored.urn.getOutSpec(), 'media:type=object;v=1', 'Should restore outSpec');
692
+
693
+ console.log(' ✓ Cap JSON serialization with mediaSpecs');
694
+ }
695
+
696
+ function testOpTagRename() {
697
+ console.log('Testing op tag (renamed from action)...');
698
+
699
+ // Should use 'op' tag, not 'action'
700
+ const cap = CapUrn.fromString(testUrn('op=generate;format=json'));
701
+ assertEqual(cap.getTag('op'), 'generate', 'Should have op tag');
702
+ assertEqual(cap.getTag('action'), undefined, 'Should not have action tag');
703
+
704
+ // Builder should use op
705
+ const built = new CapUrnBuilder()
706
+ .inSpec('media:type=void;v=1')
707
+ .outSpec('media:type=object;v=1')
708
+ .tag('op', 'transform')
709
+ .tag('type', 'data')
710
+ .build();
711
+ assertEqual(built.getTag('op'), 'transform', 'Builder should set op tag');
712
+
713
+ console.log(' ✓ Op tag (renamed from action)');
714
+ }
715
+
716
+ // ============================================================================
717
+ // MATCHING SEMANTICS SPECIFICATION TESTS
718
+ // These 9 tests verify the exact matching semantics from RULES.md Sections 12-17
719
+ // All implementations (Rust, Go, JS, ObjC) must pass these identically
720
+ // ============================================================================
721
+
722
+ function testMatchingSemantics_Test1_ExactMatch() {
723
+ console.log('Testing Matching Semantics Test 1: Exact match...');
724
+ // Test 1: Exact match (including in/out)
725
+ // Cap: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=pdf
726
+ // Request: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=pdf
727
+ // Result: MATCH
728
+ const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
729
+ const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
730
+ assert(cap.matches(request), 'Test 1: Exact match should succeed');
731
+ console.log(' ✓ Test 1: Exact match');
732
+ }
733
+
734
+ function testMatchingSemantics_Test2_CapMissingTag() {
735
+ console.log('Testing Matching Semantics Test 2: Cap missing tag...');
736
+ // Test 2: Cap missing tag (implicit wildcard for other tags, not in/out)
737
+ // Cap: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate
738
+ // Request: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=pdf
739
+ // Result: MATCH (cap can handle any ext)
740
+ const cap = CapUrn.fromString(testUrn('op=generate'));
741
+ const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
742
+ assert(cap.matches(request), 'Test 2: Cap missing tag should match (implicit wildcard)');
743
+ console.log(' ✓ Test 2: Cap missing tag');
744
+ }
745
+
746
+ function testMatchingSemantics_Test3_CapHasExtraTag() {
747
+ console.log('Testing Matching Semantics Test 3: Cap has extra tag...');
748
+ // Test 3: Cap has extra tag
749
+ // Cap: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=pdf;version=2
750
+ // Request: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=pdf
751
+ // Result: MATCH (request doesn't constrain version)
752
+ const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf;version=2'));
753
+ const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
754
+ assert(cap.matches(request), 'Test 3: Cap with extra tag should match');
755
+ console.log(' ✓ Test 3: Cap has extra tag');
756
+ }
757
+
758
+ function testMatchingSemantics_Test4_RequestHasWildcard() {
759
+ console.log('Testing Matching Semantics Test 4: Request has wildcard...');
760
+ // Test 4: Request has wildcard
761
+ // Cap: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=pdf
762
+ // Request: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=*
763
+ // Result: MATCH (request accepts any ext)
764
+ const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
765
+ const request = CapUrn.fromString(testUrn('op=generate;ext=*'));
766
+ assert(cap.matches(request), 'Test 4: Request wildcard should match');
767
+ console.log(' ✓ Test 4: Request has wildcard');
768
+ }
769
+
770
+ function testMatchingSemantics_Test5_CapHasWildcard() {
771
+ console.log('Testing Matching Semantics Test 5: Cap has wildcard...');
772
+ // Test 5: Cap has wildcard
773
+ // Cap: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=*
774
+ // Request: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=pdf
775
+ // Result: MATCH (cap handles any ext)
776
+ const cap = CapUrn.fromString(testUrn('op=generate;ext=*'));
777
+ const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
778
+ assert(cap.matches(request), 'Test 5: Cap wildcard should match');
779
+ console.log(' ✓ Test 5: Cap has wildcard');
780
+ }
781
+
782
+ function testMatchingSemantics_Test6_ValueMismatch() {
783
+ console.log('Testing Matching Semantics Test 6: Value mismatch...');
784
+ // Test 6: Value mismatch
785
+ // Cap: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=pdf
786
+ // Request: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=docx
787
+ // Result: NO MATCH
788
+ const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
789
+ const request = CapUrn.fromString(testUrn('op=generate;ext=docx'));
790
+ assert(!cap.matches(request), 'Test 6: Value mismatch should not match');
791
+ console.log(' ✓ Test 6: Value mismatch');
792
+ }
793
+
794
+ function testMatchingSemantics_Test7_FallbackPattern() {
795
+ console.log('Testing Matching Semantics Test 7: Fallback pattern...');
796
+ // Test 7: Fallback pattern
797
+ // Cap: cap:in="media:type=void;v=1";out="media:type=binary;v=1";op=generate_thumbnail
798
+ // Request: cap:in="media:type=void;v=1";out="media:type=binary;v=1";op=generate_thumbnail;ext=wav
799
+ // Result: MATCH (cap has implicit ext=*)
800
+ const cap = CapUrn.fromString('cap:in="media:type=void;v=1";out="media:type=binary;v=1";op=generate_thumbnail');
801
+ const request = CapUrn.fromString('cap:in="media:type=void;v=1";out="media:type=binary;v=1";op=generate_thumbnail;ext=wav');
802
+ assert(cap.matches(request), 'Test 7: Fallback pattern should match (cap missing ext = implicit wildcard)');
803
+ console.log(' ✓ Test 7: Fallback pattern');
804
+ }
805
+
806
+ function testMatchingSemantics_Test8_WildcardCapMatchesAnything() {
807
+ console.log('Testing Matching Semantics Test 8: Wildcard cap matches anything...');
808
+ // Test 8: Wildcard cap matches anything (replaces empty cap test)
809
+ // Cap: cap:in=*;out=*
810
+ // Request: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate;ext=pdf
811
+ // Result: MATCH
812
+ const cap = CapUrn.fromString('cap:in=*;out=*');
813
+ const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
814
+ assert(cap.matches(request), 'Test 8: Wildcard cap should match anything');
815
+ console.log(' ✓ Test 8: Wildcard cap matches anything');
816
+ }
817
+
818
+ function testMatchingSemantics_Test9_CrossDimensionIndependence() {
819
+ console.log('Testing Matching Semantics Test 9: Cross-dimension independence...');
820
+ // Test 9: Cross-dimension independence (for other tags, not in/out)
821
+ // Cap: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate
822
+ // Request: cap:in="media:type=void;v=1";out="media:type=object;v=1";ext=pdf
823
+ // Result: MATCH (both have implicit wildcards for missing tags)
824
+ const cap = CapUrn.fromString(testUrn('op=generate'));
825
+ const request = CapUrn.fromString(testUrn('ext=pdf'));
826
+ assert(cap.matches(request), 'Test 9: Cross-dimension independence should match');
827
+ console.log(' ✓ Test 9: Cross-dimension independence');
828
+ }
829
+
830
+ function testMatchingSemantics_Test10_DirectionMismatch() {
831
+ console.log('Testing Matching Semantics Test 10: Direction mismatch...');
832
+ // Test 10: Direction mismatch (in/out must match)
833
+ // Cap: cap:in="media:type=void;v=1";out="media:type=object;v=1";op=generate
834
+ // Request: cap:in="media:type=string;v=1";out="media:type=object;v=1";op=generate
835
+ // Result: NO MATCH (different inSpec)
836
+ const cap = CapUrn.fromString(testUrn('op=generate'));
837
+ const request = CapUrn.fromString('cap:in="media:type=string;v=1";out="media:type=object;v=1";op=generate');
838
+ assert(!cap.matches(request), 'Test 10: Direction mismatch should not match');
839
+ console.log(' ✓ Test 10: Direction mismatch');
840
+ }
841
+
842
+ // ============================================================================
843
+ // CapMatrix and CapCube Tests
844
+ // ============================================================================
845
+
846
+ // Helper to create a test URN
847
+ function matrixTestUrn(tags) {
848
+ if (!tags) {
849
+ return 'cap:in="media:type=void;v=1";out="media:type=object;v=1"';
850
+ }
851
+ return `cap:in="media:type=void;v=1";out="media:type=object;v=1";${tags}`;
852
+ }
853
+
854
+ // Mock CapSet for testing
855
+ class MockCapSet {
856
+ constructor(name) {
857
+ this.name = name;
858
+ }
859
+
860
+ async executeCap(capUrn, positionalArgs, namedArgs, stdinData) {
861
+ return {
862
+ binaryOutput: null,
863
+ textOutput: `Mock response from ${this.name}`
864
+ };
865
+ }
866
+ }
867
+
868
+ // Helper to create a Cap for testing
869
+ function makeCap(urnString, title) {
870
+ const capUrn = CapUrn.fromString(urnString);
871
+ return new Cap(capUrn, title, 'test', title);
872
+ }
873
+
874
+ function testCapCubeMoreSpecificWins() {
875
+ console.log('Testing CapCube: More specific wins...');
876
+
877
+ const providerRegistry = new CapMatrix();
878
+ const pluginRegistry = new CapMatrix();
879
+
880
+ // Provider: less specific cap (no ext tag)
881
+ const providerHost = new MockCapSet('provider');
882
+ const providerCap = makeCap(
883
+ 'cap:in="media:type=binary;v=1";op=generate_thumbnail;out="media:type=binary;v=1"',
884
+ 'Provider Thumbnail Generator (generic)'
885
+ );
886
+ providerRegistry.registerCapSet('provider', providerHost, [providerCap]);
887
+
888
+ // Plugin: more specific cap (has ext=pdf)
889
+ const pluginHost = new MockCapSet('plugin');
890
+ const pluginCap = makeCap(
891
+ 'cap:ext=pdf;in="media:type=binary;v=1";op=generate_thumbnail;out="media:type=binary;v=1"',
892
+ 'Plugin PDF Thumbnail Generator (specific)'
893
+ );
894
+ pluginRegistry.registerCapSet('plugin', pluginHost, [pluginCap]);
895
+
896
+ // Create composite with provider first
897
+ const composite = new CapCube();
898
+ composite.addRegistry('providers', providerRegistry);
899
+ composite.addRegistry('plugins', pluginRegistry);
900
+
901
+ // Request for PDF thumbnails - plugin's more specific cap should win
902
+ const request = 'cap:ext=pdf;in="media:type=binary;v=1";op=generate_thumbnail;out="media:type=binary;v=1"';
903
+ const best = composite.findBestCapSet(request);
904
+
905
+ assertEqual(best.registryName, 'plugins', 'More specific plugin should win');
906
+ assertEqual(best.specificity, 4, 'Plugin cap has 4 specific tags');
907
+ assertEqual(best.cap.title, 'Plugin PDF Thumbnail Generator (specific)', 'Should get plugin cap');
908
+
909
+ console.log(' ✓ More specific wins');
910
+ }
911
+
912
+ function testCapCubeTieGoesToFirst() {
913
+ console.log('Testing CapCube: Tie goes to first...');
914
+
915
+ const registry1 = new CapMatrix();
916
+ const registry2 = new CapMatrix();
917
+
918
+ // Both have same specificity
919
+ const host1 = new MockCapSet('host1');
920
+ const cap1 = makeCap(matrixTestUrn('ext=pdf;op=generate'), 'Registry 1 Cap');
921
+ registry1.registerCapSet('host1', host1, [cap1]);
922
+
923
+ const host2 = new MockCapSet('host2');
924
+ const cap2 = makeCap(matrixTestUrn('ext=pdf;op=generate'), 'Registry 2 Cap');
925
+ registry2.registerCapSet('host2', host2, [cap2]);
926
+
927
+ const composite = new CapCube();
928
+ composite.addRegistry('first', registry1);
929
+ composite.addRegistry('second', registry2);
930
+
931
+ const best = composite.findBestCapSet(matrixTestUrn('ext=pdf;op=generate'));
932
+
933
+ assertEqual(best.registryName, 'first', 'On tie, first registry should win');
934
+ assertEqual(best.cap.title, 'Registry 1 Cap', 'Should get first registry cap');
935
+
936
+ console.log(' ✓ Tie goes to first');
937
+ }
938
+
939
+ function testCapCubePollsAll() {
940
+ console.log('Testing CapCube: Polls all registries...');
941
+
942
+ const registry1 = new CapMatrix();
943
+ const registry2 = new CapMatrix();
944
+ const registry3 = new CapMatrix();
945
+
946
+ // Registry 1: doesn't match
947
+ const host1 = new MockCapSet('host1');
948
+ const cap1 = makeCap(matrixTestUrn('op=different'), 'Registry 1');
949
+ registry1.registerCapSet('host1', host1, [cap1]);
950
+
951
+ // Registry 2: matches but less specific
952
+ const host2 = new MockCapSet('host2');
953
+ const cap2 = makeCap(matrixTestUrn('op=generate'), 'Registry 2');
954
+ registry2.registerCapSet('host2', host2, [cap2]);
955
+
956
+ // Registry 3: matches and most specific
957
+ const host3 = new MockCapSet('host3');
958
+ const cap3 = makeCap(matrixTestUrn('ext=pdf;format=thumbnail;op=generate'), 'Registry 3');
959
+ registry3.registerCapSet('host3', host3, [cap3]);
960
+
961
+ const composite = new CapCube();
962
+ composite.addRegistry('r1', registry1);
963
+ composite.addRegistry('r2', registry2);
964
+ composite.addRegistry('r3', registry3);
965
+
966
+ const best = composite.findBestCapSet(matrixTestUrn('ext=pdf;format=thumbnail;op=generate'));
967
+
968
+ assertEqual(best.registryName, 'r3', 'Most specific registry should win');
969
+
970
+ console.log(' ✓ Polls all registries');
971
+ }
972
+
973
+ function testCapCubeNoMatch() {
974
+ console.log('Testing CapCube: No match error...');
975
+
976
+ const registry = new CapMatrix();
977
+ const composite = new CapCube();
978
+ composite.addRegistry('empty', registry);
979
+
980
+ try {
981
+ composite.findBestCapSet(matrixTestUrn('op=nonexistent'));
982
+ throw new Error('Expected error for non-matching capability');
983
+ } catch (e) {
984
+ assert(e instanceof CapMatrixError, 'Should be CapMatrixError');
985
+ assertEqual(e.type, 'NoSetsFound', 'Should be NoSetsFound error');
986
+ }
987
+
988
+ console.log(' ✓ No match error');
989
+ }
990
+
991
+ function testCapCubeFallbackScenario() {
992
+ console.log('Testing CapCube: Fallback scenario...');
993
+
994
+ const providerRegistry = new CapMatrix();
995
+ const pluginRegistry = new CapMatrix();
996
+
997
+ // Provider with generic fallback
998
+ const providerHost = new MockCapSet('provider_fallback');
999
+ const providerCap = makeCap(
1000
+ 'cap:in="media:type=binary;v=1";op=generate_thumbnail;out="media:type=binary;v=1"',
1001
+ 'Generic Thumbnail Provider'
1002
+ );
1003
+ providerRegistry.registerCapSet('provider_fallback', providerHost, [providerCap]);
1004
+
1005
+ // Plugin with PDF-specific handler
1006
+ const pluginHost = new MockCapSet('pdf_plugin');
1007
+ const pluginCap = makeCap(
1008
+ 'cap:ext=pdf;in="media:type=binary;v=1";op=generate_thumbnail;out="media:type=binary;v=1"',
1009
+ 'PDF Thumbnail Plugin'
1010
+ );
1011
+ pluginRegistry.registerCapSet('pdf_plugin', pluginHost, [pluginCap]);
1012
+
1013
+ const composite = new CapCube();
1014
+ composite.addRegistry('providers', providerRegistry);
1015
+ composite.addRegistry('plugins', pluginRegistry);
1016
+
1017
+ // Request for PDF thumbnail
1018
+ const request = 'cap:ext=pdf;in="media:type=binary;v=1";op=generate_thumbnail;out="media:type=binary;v=1"';
1019
+ const best = composite.findBestCapSet(request);
1020
+
1021
+ assertEqual(best.registryName, 'plugins', 'Plugin should win for PDF');
1022
+ assertEqual(best.cap.title, 'PDF Thumbnail Plugin', 'Should get plugin cap');
1023
+ assertEqual(best.specificity, 4, 'Plugin has specificity 4');
1024
+
1025
+ // Test that for a different file type, provider wins
1026
+ const requestWav = 'cap:ext=wav;in="media:type=binary;v=1";op=generate_thumbnail;out="media:type=binary;v=1"';
1027
+ const bestWav = composite.findBestCapSet(requestWav);
1028
+
1029
+ assertEqual(bestWav.registryName, 'providers', 'Provider should win for wav');
1030
+ assertEqual(bestWav.cap.title, 'Generic Thumbnail Provider', 'Should get provider cap');
1031
+
1032
+ console.log(' ✓ Fallback scenario');
1033
+ }
1034
+
1035
+ function testCapCubeCanMethod() {
1036
+ console.log('Testing CapCube: can() method...');
1037
+
1038
+ const providerRegistry = new CapMatrix();
1039
+
1040
+ const providerHost = new MockCapSet('test_provider');
1041
+ const providerCap = makeCap(matrixTestUrn('ext=pdf;op=generate'), 'Test Provider');
1042
+ providerRegistry.registerCapSet('test_provider', providerHost, [providerCap]);
1043
+
1044
+ const composite = new CapCube();
1045
+ composite.addRegistry('providers', providerRegistry);
1046
+
1047
+ // Test can() returns execution info
1048
+ const result = composite.can(matrixTestUrn('ext=pdf;op=generate'));
1049
+ assert(result.cap !== null, 'Should return cap');
1050
+ assert(result.compositeHost instanceof CompositeCapSet, 'Should return CompositeCapSet');
1051
+
1052
+ // Verify canHandle works
1053
+ assert(composite.canHandle(matrixTestUrn('ext=pdf;op=generate')), 'Should handle matching cap');
1054
+ assert(!composite.canHandle(matrixTestUrn('op=nonexistent')), 'Should not handle non-matching cap');
1055
+
1056
+ console.log(' ✓ can() method');
1057
+ }
1058
+
1059
+ function testCapCubeRegistryManagement() {
1060
+ console.log('Testing CapCube: Registry management...');
1061
+
1062
+ const composite = new CapCube();
1063
+ const registry1 = new CapMatrix();
1064
+ const registry2 = new CapMatrix();
1065
+
1066
+ // Test AddRegistry
1067
+ composite.addRegistry('r1', registry1);
1068
+ composite.addRegistry('r2', registry2);
1069
+
1070
+ let names = composite.getRegistryNames();
1071
+ assertEqual(names.length, 2, 'Should have 2 registries');
1072
+
1073
+ // Test GetRegistry
1074
+ assertEqual(composite.getRegistry('r1'), registry1, 'Should get correct registry');
1075
+
1076
+ // Test RemoveRegistry
1077
+ const removed = composite.removeRegistry('r1');
1078
+ assertEqual(removed, registry1, 'Should return removed registry');
1079
+
1080
+ names = composite.getRegistryNames();
1081
+ assertEqual(names.length, 1, 'Should have 1 registry after removal');
1082
+
1083
+ // Test GetRegistry for non-existent
1084
+ assertEqual(composite.getRegistry('nonexistent'), null, 'Should return null for non-existent');
1085
+
1086
+ console.log(' ✓ Registry management');
1087
+ }
1088
+
1089
+ // ============================================================================
1090
+ // CapGraph Tests
1091
+ // ============================================================================
1092
+
1093
+ // Helper to create caps with specific in/out media URNs for graph testing
1094
+ function makeGraphCap(inUrn, outUrn, title) {
1095
+ const urnString = `cap:in="${inUrn}";op=convert;out="${outUrn}"`;
1096
+ const capUrn = CapUrn.fromString(urnString);
1097
+ return new Cap(capUrn, title, 'convert', title);
1098
+ }
1099
+
1100
+ function testCapGraphBasicConstruction() {
1101
+ console.log('Testing CapGraph: Basic construction...');
1102
+
1103
+ const registry = new CapMatrix();
1104
+ const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
1105
+
1106
+ // binary -> str -> obj
1107
+ const cap1 = makeGraphCap('media:type=binary;v=1', 'media:type=string;v=1', 'Binary to String');
1108
+ const cap2 = makeGraphCap('media:type=string;v=1', 'media:type=object;v=1', 'String to Object');
1109
+
1110
+ registry.registerCapSet('converter', mockHost, [cap1, cap2]);
1111
+
1112
+ const cube = new CapCube();
1113
+ cube.addRegistry('converters', registry);
1114
+
1115
+ const graph = cube.graph();
1116
+
1117
+ // Check nodes
1118
+ const nodes = graph.getNodes();
1119
+ assertEqual(nodes.size, 3, 'Expected 3 nodes');
1120
+
1121
+ // Check edges
1122
+ const edges = graph.getEdges();
1123
+ assertEqual(edges.length, 2, 'Expected 2 edges');
1124
+
1125
+ // Check stats
1126
+ const stats = graph.stats();
1127
+ assertEqual(stats.nodeCount, 3, 'Expected 3 nodes in stats');
1128
+ assertEqual(stats.edgeCount, 2, 'Expected 2 edges in stats');
1129
+
1130
+ console.log(' ✓ Basic construction');
1131
+ }
1132
+
1133
+ function testCapGraphOutgoingIncoming() {
1134
+ console.log('Testing CapGraph: Outgoing and incoming edges...');
1135
+
1136
+ const registry = new CapMatrix();
1137
+ const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
1138
+
1139
+ // binary -> str, binary -> obj
1140
+ const cap1 = makeGraphCap('media:type=binary;v=1', 'media:type=string;v=1', 'Binary to String');
1141
+ const cap2 = makeGraphCap('media:type=binary;v=1', 'media:type=object;v=1', 'Binary to Object');
1142
+
1143
+ registry.registerCapSet('converter', mockHost, [cap1, cap2]);
1144
+
1145
+ const cube = new CapCube();
1146
+ cube.addRegistry('converters', registry);
1147
+
1148
+ const graph = cube.graph();
1149
+
1150
+ // binary has 2 outgoing edges
1151
+ const outgoing = graph.getOutgoing('media:type=binary;v=1');
1152
+ assertEqual(outgoing.length, 2, 'Expected 2 outgoing edges from binary');
1153
+
1154
+ // str has 1 incoming edge
1155
+ const incomingStr = graph.getIncoming('media:type=string;v=1');
1156
+ assertEqual(incomingStr.length, 1, 'Expected 1 incoming edge to str');
1157
+
1158
+ // obj has 1 incoming edge
1159
+ const incomingObj = graph.getIncoming('media:type=object;v=1');
1160
+ assertEqual(incomingObj.length, 1, 'Expected 1 incoming edge to obj');
1161
+
1162
+ console.log(' ✓ Outgoing and incoming edges');
1163
+ }
1164
+
1165
+ function testCapGraphCanConvert() {
1166
+ console.log('Testing CapGraph: Can convert...');
1167
+
1168
+ const registry = new CapMatrix();
1169
+ const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
1170
+
1171
+ // binary -> str -> obj
1172
+ const cap1 = makeGraphCap('media:type=binary;v=1', 'media:type=string;v=1', 'Binary to String');
1173
+ const cap2 = makeGraphCap('media:type=string;v=1', 'media:type=object;v=1', 'String to Object');
1174
+
1175
+ registry.registerCapSet('converter', mockHost, [cap1, cap2]);
1176
+
1177
+ const cube = new CapCube();
1178
+ cube.addRegistry('converters', registry);
1179
+
1180
+ const graph = cube.graph();
1181
+
1182
+ // Direct conversions
1183
+ assert(graph.canConvert('media:type=binary;v=1', 'media:type=string;v=1'), 'Should convert binary to str');
1184
+ assert(graph.canConvert('media:type=string;v=1', 'media:type=object;v=1'), 'Should convert str to obj');
1185
+
1186
+ // Transitive conversion
1187
+ assert(graph.canConvert('media:type=binary;v=1', 'media:type=object;v=1'), 'Should convert binary to obj transitively');
1188
+
1189
+ // Same spec
1190
+ assert(graph.canConvert('media:type=binary;v=1', 'media:type=binary;v=1'), 'Should convert same spec');
1191
+
1192
+ // Impossible conversions
1193
+ assert(!graph.canConvert('media:type=object;v=1', 'media:type=binary;v=1'), 'Should not convert obj to binary');
1194
+ assert(!graph.canConvert('media:type=nonexistent;v=1', 'media:type=string;v=1'), 'Should not convert nonexistent');
1195
+
1196
+ console.log(' ✓ Can convert');
1197
+ }
1198
+
1199
+ function testCapGraphFindPath() {
1200
+ console.log('Testing CapGraph: Find path...');
1201
+
1202
+ const registry = new CapMatrix();
1203
+ const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
1204
+
1205
+ // binary -> str -> obj
1206
+ const cap1 = makeGraphCap('media:type=binary;v=1', 'media:type=string;v=1', 'Binary to String');
1207
+ const cap2 = makeGraphCap('media:type=string;v=1', 'media:type=object;v=1', 'String to Object');
1208
+
1209
+ registry.registerCapSet('converter', mockHost, [cap1, cap2]);
1210
+
1211
+ const cube = new CapCube();
1212
+ cube.addRegistry('converters', registry);
1213
+
1214
+ const graph = cube.graph();
1215
+
1216
+ // Direct path
1217
+ let path = graph.findPath('media:type=binary;v=1', 'media:type=string;v=1');
1218
+ assert(path !== null, 'Should find path from binary to str');
1219
+ assertEqual(path.length, 1, 'Expected path length 1');
1220
+
1221
+ // Transitive path
1222
+ path = graph.findPath('media:type=binary;v=1', 'media:type=object;v=1');
1223
+ assert(path !== null, 'Should find path from binary to obj');
1224
+ assertEqual(path.length, 2, 'Expected path length 2');
1225
+ assertEqual(path[0].cap.title, 'Binary to String', 'First edge');
1226
+ assertEqual(path[1].cap.title, 'String to Object', 'Second edge');
1227
+
1228
+ // No path
1229
+ path = graph.findPath('media:type=object;v=1', 'media:type=binary;v=1');
1230
+ assertEqual(path, null, 'Should not find impossible path');
1231
+
1232
+ // Same spec
1233
+ path = graph.findPath('media:type=binary;v=1', 'media:type=binary;v=1');
1234
+ assert(path !== null, 'Should return empty path for same spec');
1235
+ assertEqual(path.length, 0, 'Expected empty path for same spec');
1236
+
1237
+ console.log(' ✓ Find path');
1238
+ }
1239
+
1240
+ function testCapGraphFindAllPaths() {
1241
+ console.log('Testing CapGraph: Find all paths...');
1242
+
1243
+ const registry = new CapMatrix();
1244
+ const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
1245
+
1246
+ // binary -> str -> obj
1247
+ // binary -> obj (direct)
1248
+ const cap1 = makeGraphCap('media:type=binary;v=1', 'media:type=string;v=1', 'Binary to String');
1249
+ const cap2 = makeGraphCap('media:type=string;v=1', 'media:type=object;v=1', 'String to Object');
1250
+ const cap3 = makeGraphCap('media:type=binary;v=1', 'media:type=object;v=1', 'Binary to Object (direct)');
1251
+
1252
+ registry.registerCapSet('converter', mockHost, [cap1, cap2, cap3]);
1253
+
1254
+ const cube = new CapCube();
1255
+ cube.addRegistry('converters', registry);
1256
+
1257
+ const graph = cube.graph();
1258
+
1259
+ // Find all paths from binary to obj
1260
+ const paths = graph.findAllPaths('media:type=binary;v=1', 'media:type=object;v=1', 3);
1261
+
1262
+ assertEqual(paths.length, 2, 'Expected 2 paths');
1263
+
1264
+ // Paths should be sorted by length (shortest first)
1265
+ assertEqual(paths[0].length, 1, 'First path should have length 1 (direct)');
1266
+ assertEqual(paths[1].length, 2, 'Second path should have length 2 (via str)');
1267
+
1268
+ console.log(' ✓ Find all paths');
1269
+ }
1270
+
1271
+ function testCapGraphGetDirectEdges() {
1272
+ console.log('Testing CapGraph: Get direct edges...');
1273
+
1274
+ const registry1 = new CapMatrix();
1275
+ const registry2 = new CapMatrix();
1276
+ const mockHost1 = { executeCap: async () => ({ textOutput: 'mock1' }) };
1277
+ const mockHost2 = { executeCap: async () => ({ textOutput: 'mock2' }) };
1278
+
1279
+ // Two converters: binary -> str with different specificities
1280
+ const cap1 = makeGraphCap('media:type=binary;v=1', 'media:type=string;v=1', 'Generic Binary to String');
1281
+
1282
+ // More specific converter (with extra tag for higher specificity)
1283
+ const capUrn2 = CapUrn.fromString('cap:ext=pdf;in="media:type=binary;v=1";op=convert;out="media:type=string;v=1"');
1284
+ const cap2 = new Cap(capUrn2, 'PDF Binary to String', 'convert', 'PDF Binary to String');
1285
+
1286
+ registry1.registerCapSet('converter1', mockHost1, [cap1]);
1287
+ registry2.registerCapSet('converter2', mockHost2, [cap2]);
1288
+
1289
+ const cube = new CapCube();
1290
+ cube.addRegistry('reg1', registry1);
1291
+ cube.addRegistry('reg2', registry2);
1292
+
1293
+ const graph = cube.graph();
1294
+
1295
+ // Get direct edges (should be sorted by specificity)
1296
+ const edges = graph.getDirectEdges('media:type=binary;v=1', 'media:type=string;v=1');
1297
+
1298
+ assertEqual(edges.length, 2, 'Expected 2 direct edges');
1299
+
1300
+ // First should be more specific (PDF converter)
1301
+ assertEqual(edges[0].cap.title, 'PDF Binary to String', 'First edge should be more specific');
1302
+ assert(edges[0].specificity > edges[1].specificity, 'First edge should have higher specificity');
1303
+
1304
+ console.log(' ✓ Get direct edges');
1305
+ }
1306
+
1307
+ function testCapGraphStats() {
1308
+ console.log('Testing CapGraph: Stats...');
1309
+
1310
+ const registry = new CapMatrix();
1311
+ const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
1312
+
1313
+ // binary -> str -> obj
1314
+ // \-> json
1315
+ const cap1 = makeGraphCap('media:type=binary;v=1', 'media:type=string;v=1', 'Binary to String');
1316
+ const cap2 = makeGraphCap('media:type=string;v=1', 'media:type=object;v=1', 'String to Object');
1317
+ const cap3 = makeGraphCap('media:type=binary;v=1', 'media:type=json;v=1', 'Binary to JSON');
1318
+
1319
+ registry.registerCapSet('converter', mockHost, [cap1, cap2, cap3]);
1320
+
1321
+ const cube = new CapCube();
1322
+ cube.addRegistry('converters', registry);
1323
+
1324
+ const graph = cube.graph();
1325
+ const stats = graph.stats();
1326
+
1327
+ // 4 unique nodes: binary, str, obj, json
1328
+ assertEqual(stats.nodeCount, 4, 'Expected 4 nodes');
1329
+
1330
+ // 3 edges
1331
+ assertEqual(stats.edgeCount, 3, 'Expected 3 edges');
1332
+
1333
+ // 2 input URNs (binary, str)
1334
+ assertEqual(stats.inputUrnCount, 2, 'Expected 2 input URNs');
1335
+
1336
+ // 3 output URNs (str, obj, json)
1337
+ assertEqual(stats.outputUrnCount, 3, 'Expected 3 output URNs');
1338
+
1339
+ console.log(' ✓ Stats');
1340
+ }
1341
+
1342
+ function testCapGraphWithCapCube() {
1343
+ console.log('Testing CapGraph: With CapCube...');
1344
+
1345
+ // Integration test: build graph from CapCube
1346
+ const providerRegistry = new CapMatrix();
1347
+ const pluginRegistry = new CapMatrix();
1348
+ const providerHost = { executeCap: async () => ({ textOutput: 'provider' }) };
1349
+ const pluginHost = { executeCap: async () => ({ textOutput: 'plugin' }) };
1350
+
1351
+ // Provider: binary -> str
1352
+ const providerCap = makeGraphCap('media:type=binary;v=1', 'media:type=string;v=1', 'Provider Binary to String');
1353
+ providerRegistry.registerCapSet('provider', providerHost, [providerCap]);
1354
+
1355
+ // Plugin: str -> obj
1356
+ const pluginCap = makeGraphCap('media:type=string;v=1', 'media:type=object;v=1', 'Plugin String to Object');
1357
+ pluginRegistry.registerCapSet('plugin', pluginHost, [pluginCap]);
1358
+
1359
+ const cube = new CapCube();
1360
+ cube.addRegistry('providers', providerRegistry);
1361
+ cube.addRegistry('plugins', pluginRegistry);
1362
+
1363
+ const graph = cube.graph();
1364
+
1365
+ // Should be able to convert binary -> obj through both registries
1366
+ assert(graph.canConvert('media:type=binary;v=1', 'media:type=object;v=1'), 'Should convert across registries');
1367
+
1368
+ const path = graph.findPath('media:type=binary;v=1', 'media:type=object;v=1');
1369
+ assert(path !== null, 'Should find path');
1370
+ assertEqual(path.length, 2, 'Expected path length 2');
1371
+
1372
+ // Verify edges come from different registries
1373
+ assertEqual(path[0].registryName, 'providers', 'First edge from providers');
1374
+ assertEqual(path[1].registryName, 'plugins', 'Second edge from plugins');
1375
+
1376
+ console.log(' ✓ With CapCube');
1377
+ }
1378
+
1379
+ // Update runTests to include new tests
1380
+ function runTests() {
1381
+ console.log('Running Cap URN JavaScript tests...\n');
1382
+
1383
+ // Original URN tests
1384
+ testCapUrnCreation();
1385
+ testCaseInsensitive();
1386
+ testCapPrefixRequired();
1387
+ testTrailingSemicolonEquivalence();
1388
+ testCanonicalStringFormat();
1389
+ testTagMatching();
1390
+ testMissingTagHandling();
1391
+ testSpecificity();
1392
+ testCompatibility();
1393
+ testBuilder();
1394
+ testConvenienceMethods();
1395
+ testCapMatcher();
1396
+ testJSONSerialization();
1397
+ testEmptyCapUrn();
1398
+ testExtendedCharacterSupport();
1399
+ testWildcardRestrictions();
1400
+ testDuplicateKeyRejection();
1401
+ testNumericKeyRestriction();
1402
+
1403
+ // New format tests
1404
+ testMediaSpecCanonicalFormat();
1405
+ testMediaSpecLegacyFormatRejection();
1406
+ testBuiltinSpecIds();
1407
+ testSpecIdResolution();
1408
+ testMediaUrnResolutionWithMediaSpecs();
1409
+ testMediaUrnResolutionFailHard();
1410
+ testCapWithMediaSpecs();
1411
+ testCapJSONSerialization();
1412
+ testOpTagRename();
1413
+
1414
+ // Matching semantics specification tests (10 tests with direction support)
1415
+ testMatchingSemantics_Test1_ExactMatch();
1416
+ testMatchingSemantics_Test2_CapMissingTag();
1417
+ testMatchingSemantics_Test3_CapHasExtraTag();
1418
+ testMatchingSemantics_Test4_RequestHasWildcard();
1419
+ testMatchingSemantics_Test5_CapHasWildcard();
1420
+ testMatchingSemantics_Test6_ValueMismatch();
1421
+ testMatchingSemantics_Test7_FallbackPattern();
1422
+ testMatchingSemantics_Test8_WildcardCapMatchesAnything();
1423
+ testMatchingSemantics_Test9_CrossDimensionIndependence();
1424
+ testMatchingSemantics_Test10_DirectionMismatch();
1425
+
1426
+ // CapMatrix and CapCube tests
1427
+ testCapCubeMoreSpecificWins();
1428
+ testCapCubeTieGoesToFirst();
1429
+ testCapCubePollsAll();
1430
+ testCapCubeNoMatch();
1431
+ testCapCubeFallbackScenario();
1432
+ testCapCubeCanMethod();
1433
+ testCapCubeRegistryManagement();
1434
+
1435
+ // CapGraph tests
1436
+ testCapGraphBasicConstruction();
1437
+ testCapGraphOutgoingIncoming();
1438
+ testCapGraphCanConvert();
1439
+ testCapGraphFindPath();
1440
+ testCapGraphFindAllPaths();
1441
+ testCapGraphGetDirectEdges();
1442
+ testCapGraphStats();
1443
+ testCapGraphWithCapCube();
1444
+
1445
+ console.log('OK All tests passed!');
1446
+ }
1447
+
1448
+ // Run the tests
1449
+ if (require.main === module) {
1450
+ try {
1451
+ runTests();
1452
+ process.exit(0);
1453
+ } catch (error) {
1454
+ console.error('\nERR Test failed:', error.message);
1455
+ process.exit(1);
1456
+ }
1457
+ }
1458
+
1459
+ module.exports = { runTests };