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.
- package/README.md +130 -0
- package/RULES.md +55 -0
- package/capns.js +2834 -0
- package/capns.test.js +1459 -0
- 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 };
|