capdag 0.88.20458
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 +131 -0
- package/RULES.md +111 -0
- package/capdag.js +4322 -0
- package/capdag.test.js +2874 -0
- package/package.json +36 -0
package/capdag.test.js
ADDED
|
@@ -0,0 +1,2874 @@
|
|
|
1
|
+
// Cap URN JavaScript Test Suite
|
|
2
|
+
// Tests mirror Rust test numbering (TEST###) for cross-language tracking.
|
|
3
|
+
// All implementations (Rust, Go, JS, ObjC, Python) must pass these identically.
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
CapUrn, CapUrnBuilder, CapMatcher, CapUrnError, ErrorCodes,
|
|
7
|
+
MediaUrn, MediaUrnError, MediaUrnErrorCodes,
|
|
8
|
+
Cap, MediaSpec, MediaSpecError, MediaSpecErrorCodes,
|
|
9
|
+
resolveMediaUrn, buildExtensionIndex, mediaUrnsForExtension, getExtensionMappings,
|
|
10
|
+
CapMatrixError, CapMatrix, BestCapSetMatch, CompositeCapSet, CapBlock,
|
|
11
|
+
PluginInfo, PluginCapSummary, PluginSuggestion, PluginRepoClient, PluginRepoServer,
|
|
12
|
+
CapGraphEdge, CapGraphStats, CapGraph,
|
|
13
|
+
StdinSource, StdinSourceKind,
|
|
14
|
+
validateNoMediaSpecRedefinitionSync,
|
|
15
|
+
CapArgumentValue,
|
|
16
|
+
llmConversationUrn, modelAvailabilityUrn, modelPathUrn,
|
|
17
|
+
MEDIA_STRING, MEDIA_INTEGER, MEDIA_NUMBER, MEDIA_BOOLEAN,
|
|
18
|
+
MEDIA_OBJECT, MEDIA_STRING_ARRAY, MEDIA_INTEGER_ARRAY,
|
|
19
|
+
MEDIA_NUMBER_ARRAY, MEDIA_BOOLEAN_ARRAY, MEDIA_OBJECT_ARRAY,
|
|
20
|
+
MEDIA_BINARY, MEDIA_VOID, MEDIA_PNG, MEDIA_AUDIO, MEDIA_VIDEO,
|
|
21
|
+
MEDIA_PDF, MEDIA_EPUB, MEDIA_MD, MEDIA_TXT, MEDIA_RST, MEDIA_LOG,
|
|
22
|
+
MEDIA_HTML, MEDIA_XML, MEDIA_JSON, MEDIA_YAML, MEDIA_JSON_SCHEMA,
|
|
23
|
+
MEDIA_MODEL_SPEC, MEDIA_AVAILABILITY_OUTPUT, MEDIA_PATH_OUTPUT,
|
|
24
|
+
MEDIA_LLM_INFERENCE_OUTPUT,
|
|
25
|
+
MEDIA_FILE_PATH, MEDIA_FILE_PATH_ARRAY,
|
|
26
|
+
MEDIA_COLLECTION, MEDIA_COLLECTION_LIST,
|
|
27
|
+
MEDIA_DECISION, MEDIA_DECISION_ARRAY,
|
|
28
|
+
MEDIA_AUDIO_SPEECH, MEDIA_IMAGE_THUMBNAIL
|
|
29
|
+
} = require('./capdag.js');
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Test utilities
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
let passCount = 0;
|
|
36
|
+
let failCount = 0;
|
|
37
|
+
|
|
38
|
+
function assert(condition, message) {
|
|
39
|
+
if (!condition) {
|
|
40
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertEqual(actual, expected, message) {
|
|
45
|
+
if (actual !== expected) {
|
|
46
|
+
throw new Error(`Assertion failed: ${message}. Expected: ${JSON.stringify(expected)}, Actual: ${JSON.stringify(actual)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertThrows(fn, expectedErrorCode, message) {
|
|
51
|
+
try {
|
|
52
|
+
fn();
|
|
53
|
+
throw new Error(`Expected error but function succeeded: ${message}`);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.message && error.message.startsWith('Expected error but function succeeded')) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
if (error instanceof CapUrnError && error.code === expectedErrorCode) {
|
|
59
|
+
return; // Expected error
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Expected CapUrnError with code ${expectedErrorCode} but got: [code=${error.code}] ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function assertThrowsMediaUrn(fn, expectedErrorCode, message) {
|
|
66
|
+
try {
|
|
67
|
+
fn();
|
|
68
|
+
throw new Error(`Expected error but function succeeded: ${message}`);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.message && error.message.startsWith('Expected error but function succeeded')) {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
if (error instanceof MediaUrnError && error.code === expectedErrorCode) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Expected MediaUrnError with code ${expectedErrorCode} but got: [code=${error.code}] ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runTest(name, fn) {
|
|
81
|
+
try {
|
|
82
|
+
const result = fn();
|
|
83
|
+
if (result && typeof result.then === 'function') {
|
|
84
|
+
return result.then(() => {
|
|
85
|
+
passCount++;
|
|
86
|
+
console.log(` PASS ${name}`);
|
|
87
|
+
}).catch(err => {
|
|
88
|
+
failCount++;
|
|
89
|
+
console.log(` FAIL ${name}: ${err.message}`);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
passCount++;
|
|
93
|
+
console.log(` PASS ${name}`);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
failCount++;
|
|
96
|
+
console.log(` FAIL ${name}: ${err.message}`);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Helper function to build test URNs with required in/out media URNs.
|
|
103
|
+
* Uses MEDIA_VOID for in and MEDIA_OBJECT for out, matching the
|
|
104
|
+
* Rust reference test_urn helper: test_urn(tags) => cap:in="media:void";{tags};out="media:record;textable"
|
|
105
|
+
*/
|
|
106
|
+
function testUrn(tags) {
|
|
107
|
+
if (!tags || tags === '') {
|
|
108
|
+
return `cap:in="${MEDIA_VOID}";out="${MEDIA_OBJECT}"`;
|
|
109
|
+
}
|
|
110
|
+
return `cap:in="${MEDIA_VOID}";${tags};out="${MEDIA_OBJECT}"`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Mock CapSet for testing
|
|
114
|
+
class MockCapSet {
|
|
115
|
+
constructor(name) {
|
|
116
|
+
this.name = name;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async executeCap(capUrn, args) {
|
|
120
|
+
return {
|
|
121
|
+
binaryOutput: null,
|
|
122
|
+
textOutput: `Mock response from ${this.name}`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Helper to create a Cap for testing
|
|
128
|
+
function makeCap(urnString, title) {
|
|
129
|
+
const capUrn = CapUrn.fromString(urnString);
|
|
130
|
+
return new Cap(capUrn, title, 'test', title);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Helper to create caps with specific in/out media URNs for graph testing
|
|
134
|
+
function makeGraphCap(inUrn, outUrn, title) {
|
|
135
|
+
const urnString = `cap:in="${inUrn}";op=convert;out="${outUrn}"`;
|
|
136
|
+
const capUrn = CapUrn.fromString(urnString);
|
|
137
|
+
return new Cap(capUrn, title, 'convert', title);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Helper to create a test URN for matrix tests
|
|
141
|
+
function matrixTestUrn(tags) {
|
|
142
|
+
if (!tags) {
|
|
143
|
+
return 'cap:in="media:void";out="media:object"';
|
|
144
|
+
}
|
|
145
|
+
return `cap:in="media:void";out="media:object";${tags}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// cap_urn.rs: TEST001-TEST050, TEST890-TEST891
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
// TEST001: Cap URN created with tags, direction specs accessible
|
|
153
|
+
function test001_capUrnCreation() {
|
|
154
|
+
const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf;target=thumbnail'));
|
|
155
|
+
assertEqual(cap.getTag('op'), 'generate', 'Should get op tag');
|
|
156
|
+
assertEqual(cap.getTag('target'), 'thumbnail', 'Should get target tag');
|
|
157
|
+
assertEqual(cap.getTag('ext'), 'pdf', 'Should get ext tag');
|
|
158
|
+
assertEqual(cap.getInSpec(), MEDIA_VOID, 'Should get inSpec');
|
|
159
|
+
assertEqual(cap.getOutSpec(), MEDIA_OBJECT, 'Should get outSpec');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// TEST002: Missing in -> MissingInSpec, missing out -> MissingOutSpec
|
|
163
|
+
function test002_directionSpecsRequired() {
|
|
164
|
+
assertThrows(
|
|
165
|
+
() => CapUrn.fromString('cap:out="media:void";op=test'),
|
|
166
|
+
ErrorCodes.MISSING_IN_SPEC,
|
|
167
|
+
'Missing in should fail with MISSING_IN_SPEC'
|
|
168
|
+
);
|
|
169
|
+
assertThrows(
|
|
170
|
+
() => CapUrn.fromString('cap:in="media:void";op=test'),
|
|
171
|
+
ErrorCodes.MISSING_OUT_SPEC,
|
|
172
|
+
'Missing out should fail with MISSING_OUT_SPEC'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// TEST003: Direction specs must match exactly; wildcard matches any
|
|
177
|
+
function test003_directionMatching() {
|
|
178
|
+
const cap = CapUrn.fromString(testUrn('op=generate'));
|
|
179
|
+
const request = CapUrn.fromString(testUrn('op=generate'));
|
|
180
|
+
assert(cap.accepts(request), 'Same direction specs should match');
|
|
181
|
+
|
|
182
|
+
// Different direction should not match
|
|
183
|
+
const requestDiff = CapUrn.fromString('cap:in="media:textable";op=generate;out="media:record;textable"');
|
|
184
|
+
assert(!cap.accepts(requestDiff), 'Different inSpec should not match');
|
|
185
|
+
|
|
186
|
+
// Wildcard direction matches any
|
|
187
|
+
const wildcardCap = CapUrn.fromString('cap:in=*;op=generate;out=*');
|
|
188
|
+
assert(wildcardCap.accepts(request), 'Wildcard direction should match any');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// TEST004: Unquoted keys/values normalized to lowercase
|
|
192
|
+
function test004_unquotedValuesLowercased() {
|
|
193
|
+
const cap = CapUrn.fromString('cap:IN="media:void";OP=Generate;EXT=PDF;OUT="media:record;textable"');
|
|
194
|
+
assertEqual(cap.getTag('op'), 'generate', 'Unquoted value should be lowercased');
|
|
195
|
+
assertEqual(cap.getTag('ext'), 'pdf', 'Unquoted value should be lowercased');
|
|
196
|
+
// Key lookup is case-insensitive
|
|
197
|
+
assertEqual(cap.getTag('OP'), 'generate', 'Key lookup should be case-insensitive');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// TEST005: Quoted values preserve case
|
|
201
|
+
function test005_quotedValuesPreserveCase() {
|
|
202
|
+
const cap = CapUrn.fromString('cap:in="media:void";key="HelloWorld";out="media:void"');
|
|
203
|
+
assertEqual(cap.getTag('key'), 'HelloWorld', 'Quoted value should preserve case');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// TEST006: Semicolons, equals, spaces in quoted values
|
|
207
|
+
function test006_quotedValueSpecialChars() {
|
|
208
|
+
const cap = CapUrn.fromString('cap:in="media:void";key="val;ue=with spaces";out="media:void"');
|
|
209
|
+
assertEqual(cap.getTag('key'), 'val;ue=with spaces', 'Quoted value should preserve special chars');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// TEST007: Escaped quotes and backslashes in quoted values
|
|
213
|
+
function test007_quotedValueEscapeSequences() {
|
|
214
|
+
const s = String.raw`cap:in="media:void";key="val\"ue\\test";out="media:void"`;
|
|
215
|
+
const cap = CapUrn.fromString(s);
|
|
216
|
+
assertEqual(cap.getTag('key'), 'val"ue\\test', 'Escaped quote and backslash should be unescaped');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// TEST008: Mix of quoted and unquoted values
|
|
220
|
+
function test008_mixedQuotedUnquoted() {
|
|
221
|
+
const cap = CapUrn.fromString('cap:a=simple;b="Quoted";in="media:void";out="media:void"');
|
|
222
|
+
assertEqual(cap.getTag('a'), 'simple', 'Unquoted value should be lowercase');
|
|
223
|
+
assertEqual(cap.getTag('b'), 'Quoted', 'Quoted value should preserve case');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// TEST009: Unterminated quote produces error
|
|
227
|
+
function test009_unterminatedQuoteError() {
|
|
228
|
+
let threw = false;
|
|
229
|
+
try {
|
|
230
|
+
CapUrn.fromString('cap:in="media:void";key="unterminated;out="media:void"');
|
|
231
|
+
} catch (e) {
|
|
232
|
+
if (e instanceof CapUrnError) {
|
|
233
|
+
threw = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
assert(threw, 'Unterminated quote should produce CapUrnError');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// TEST010: Invalid escape sequences produce error
|
|
240
|
+
function test010_invalidEscapeSequenceError() {
|
|
241
|
+
let threw = false;
|
|
242
|
+
try {
|
|
243
|
+
const s = String.raw`cap:in="media:void";key="hello\x";out="media:void"`;
|
|
244
|
+
CapUrn.fromString(s);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
if (e instanceof CapUrnError) {
|
|
247
|
+
threw = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
assert(threw, 'Invalid escape sequence should produce CapUrnError');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// TEST011: Smart quoting: no quotes for simple lowercase, quotes for special
|
|
254
|
+
function test011_serializationSmartQuoting() {
|
|
255
|
+
const cap = CapUrn.fromString('cap:a=simple;b="Has Space";in="media:void";out="media:void"');
|
|
256
|
+
const s = cap.toString();
|
|
257
|
+
// simple lowercase should not be quoted, "Has Space" should be quoted
|
|
258
|
+
assert(s.includes('a=simple'), 'Simple value should not be quoted');
|
|
259
|
+
assert(s.includes('b="Has Space"'), 'Value with space should be quoted');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// TEST012: Simple cap URN parse -> serialize -> parse equals original
|
|
263
|
+
function test012_roundTripSimple() {
|
|
264
|
+
const original = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
265
|
+
const serialized = original.toString();
|
|
266
|
+
const reparsed = CapUrn.fromString(serialized);
|
|
267
|
+
assert(original.equals(reparsed), 'Simple round-trip should produce equal URN');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// TEST013: Quoted values round-trip preserving case
|
|
271
|
+
function test013_roundTripQuoted() {
|
|
272
|
+
const original = CapUrn.fromString('cap:in="media:void";key="HelloWorld";out="media:void"');
|
|
273
|
+
const serialized = original.toString();
|
|
274
|
+
const reparsed = CapUrn.fromString(serialized);
|
|
275
|
+
assert(original.equals(reparsed), 'Quoted round-trip should produce equal URN');
|
|
276
|
+
assertEqual(reparsed.getTag('key'), 'HelloWorld', 'Quoted value should survive round-trip');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// TEST014: Escape sequences round-trip correctly
|
|
280
|
+
function test014_roundTripEscapes() {
|
|
281
|
+
const s = String.raw`cap:in="media:void";key="val\"ue\\test";out="media:void"`;
|
|
282
|
+
const original = CapUrn.fromString(s);
|
|
283
|
+
const serialized = original.toString();
|
|
284
|
+
const reparsed = CapUrn.fromString(serialized);
|
|
285
|
+
assert(original.equals(reparsed), 'Escape round-trip should produce equal URN');
|
|
286
|
+
assertEqual(reparsed.getTag('key'), 'val"ue\\test', 'Escaped value should survive round-trip');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// TEST015: cap: prefix required, case-insensitive
|
|
290
|
+
function test015_capPrefixRequired() {
|
|
291
|
+
assertThrows(
|
|
292
|
+
() => CapUrn.fromString('in="media:void";out="media:void";op=generate'),
|
|
293
|
+
ErrorCodes.MISSING_CAP_PREFIX,
|
|
294
|
+
'Should require cap: prefix'
|
|
295
|
+
);
|
|
296
|
+
// Valid cap: prefix should work
|
|
297
|
+
const cap = CapUrn.fromString(testUrn('op=generate'));
|
|
298
|
+
assertEqual(cap.getTag('op'), 'generate', 'Should parse with valid cap: prefix');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// TEST016: With/without trailing semicolon are equivalent
|
|
302
|
+
function test016_trailingSemicolonEquivalence() {
|
|
303
|
+
const cap1 = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
304
|
+
const cap2 = CapUrn.fromString(testUrn('op=generate;ext=pdf') + ';');
|
|
305
|
+
assert(cap1.equals(cap2), 'With/without trailing semicolon should be equal');
|
|
306
|
+
assertEqual(cap1.toString(), cap2.toString(), 'Canonical forms should match');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// TEST017: Exact match, subset match, wildcard match, value mismatch
|
|
310
|
+
function test017_tagMatching() {
|
|
311
|
+
const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf;target=thumbnail'));
|
|
312
|
+
|
|
313
|
+
// Exact match
|
|
314
|
+
const exact = CapUrn.fromString(testUrn('op=generate;ext=pdf;target=thumbnail'));
|
|
315
|
+
assert(cap.accepts(exact), 'Should accept exact match');
|
|
316
|
+
|
|
317
|
+
// Subset match
|
|
318
|
+
const subset = CapUrn.fromString(testUrn('op=generate'));
|
|
319
|
+
assert(cap.accepts(subset), 'Should accept subset request');
|
|
320
|
+
|
|
321
|
+
// Wildcard match
|
|
322
|
+
const wildcard = CapUrn.fromString(testUrn('ext=*'));
|
|
323
|
+
assert(cap.accepts(wildcard), 'Should accept wildcard request');
|
|
324
|
+
|
|
325
|
+
// Value mismatch
|
|
326
|
+
const mismatch = CapUrn.fromString(testUrn('op=extract'));
|
|
327
|
+
assert(!cap.accepts(mismatch), 'Should not accept value mismatch');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// TEST018: Quoted uppercase values don't match lowercase (case sensitive)
|
|
331
|
+
function test018_matchingCaseSensitiveValues() {
|
|
332
|
+
const cap = CapUrn.fromString('cap:in="media:void";key="HelloWorld";out="media:void"');
|
|
333
|
+
const request = CapUrn.fromString('cap:in="media:void";key=helloworld;out="media:void"');
|
|
334
|
+
assert(!cap.accepts(request), 'Quoted HelloWorld should not match unquoted helloworld');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// TEST019: Missing tags treated as wildcards
|
|
338
|
+
function test019_missingTagHandling() {
|
|
339
|
+
const cap = CapUrn.fromString(testUrn('op=generate'));
|
|
340
|
+
|
|
341
|
+
// Request with tag cap doesn't have -> cap's missing tag is implicit wildcard
|
|
342
|
+
const request = CapUrn.fromString(testUrn('ext=pdf'));
|
|
343
|
+
assert(cap.accepts(request), 'Missing tag in cap should be treated as wildcard');
|
|
344
|
+
|
|
345
|
+
// Cap with extra tags can accept subset requests
|
|
346
|
+
const cap2 = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
347
|
+
const request2 = CapUrn.fromString(testUrn('op=generate'));
|
|
348
|
+
assert(cap2.accepts(request2), 'Cap with extra tags should accept subset request');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// TEST020: Direction specs use MediaUrn tag count, other tags count non-wildcard
|
|
352
|
+
function test020_specificity() {
|
|
353
|
+
// Direction specs contribute their MediaUrn tag count:
|
|
354
|
+
// MEDIA_VOID = "media:void" -> 1 tag (void)
|
|
355
|
+
// MEDIA_OBJECT = "media:record" -> 1 tag (record)
|
|
356
|
+
const cap1 = CapUrn.fromString(testUrn('type=general'));
|
|
357
|
+
assertEqual(cap1.specificity(), 3, 'void(1) + record(1) + type(1)');
|
|
358
|
+
|
|
359
|
+
const cap2 = CapUrn.fromString(testUrn('op=generate'));
|
|
360
|
+
assertEqual(cap2.specificity(), 3, 'void(1) + record(1) + op(1)');
|
|
361
|
+
|
|
362
|
+
const cap3 = CapUrn.fromString(testUrn('op=*;ext=pdf'));
|
|
363
|
+
assertEqual(cap3.specificity(), 3, 'void(1) + record(1) + ext(1) (wildcard op doesn\'t count)');
|
|
364
|
+
|
|
365
|
+
// Wildcard in direction doesn't count
|
|
366
|
+
const cap4 = CapUrn.fromString(`cap:in=*;out="${MEDIA_OBJECT}";op=test`);
|
|
367
|
+
assertEqual(cap4.specificity(), 2, 'record(1) + op(1) (in wildcard doesn\'t count)');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// TEST021: CapUrnBuilder creates valid URN
|
|
371
|
+
function test021_builder() {
|
|
372
|
+
const cap = new CapUrnBuilder()
|
|
373
|
+
.inSpec('media:void')
|
|
374
|
+
.outSpec('media:object')
|
|
375
|
+
.tag('op', 'generate')
|
|
376
|
+
.tag('ext', 'pdf')
|
|
377
|
+
.build();
|
|
378
|
+
assertEqual(cap.getTag('op'), 'generate', 'Builder should set op');
|
|
379
|
+
assertEqual(cap.getTag('ext'), 'pdf', 'Builder should set ext');
|
|
380
|
+
assertEqual(cap.getInSpec(), 'media:void', 'Builder should set inSpec');
|
|
381
|
+
assertEqual(cap.getOutSpec(), 'media:object', 'Builder should set outSpec');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// TEST022: Builder requires both inSpec and outSpec
|
|
385
|
+
function test022_builderRequiresDirection() {
|
|
386
|
+
assertThrows(
|
|
387
|
+
() => new CapUrnBuilder().tag('op', 'test').build(),
|
|
388
|
+
ErrorCodes.MISSING_IN_SPEC,
|
|
389
|
+
'Builder should require inSpec'
|
|
390
|
+
);
|
|
391
|
+
assertThrows(
|
|
392
|
+
() => new CapUrnBuilder().inSpec('media:void').tag('op', 'test').build(),
|
|
393
|
+
ErrorCodes.MISSING_OUT_SPEC,
|
|
394
|
+
'Builder should require outSpec'
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// TEST023: Builder lowercases keys but preserves value case
|
|
399
|
+
function test023_builderPreservesCase() {
|
|
400
|
+
const cap = new CapUrnBuilder()
|
|
401
|
+
.inSpec('media:void')
|
|
402
|
+
.outSpec('media:void')
|
|
403
|
+
.tag('MyKey', 'MyValue')
|
|
404
|
+
.build();
|
|
405
|
+
assertEqual(cap.getTag('mykey'), 'MyValue', 'Builder should lowercase key but preserve value case');
|
|
406
|
+
assertEqual(cap.getTag('MyKey'), 'MyValue', 'getTag should be case-insensitive for keys');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// TEST024: Directional accepts checks
|
|
410
|
+
function test024_compatibility() {
|
|
411
|
+
// General cap accepts specific request (missing tags = wildcards)
|
|
412
|
+
const general = CapUrn.fromString(testUrn('op=generate'));
|
|
413
|
+
const specific = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
414
|
+
assert(general.accepts(specific), 'General cap should accept specific request');
|
|
415
|
+
// Specific cap also accepts general request (cap has extra tag, not blocking)
|
|
416
|
+
assert(specific.accepts(general), 'Specific cap accepts general request (extra tags ok)');
|
|
417
|
+
|
|
418
|
+
// Different op values: neither accepts the other
|
|
419
|
+
const cap1 = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
420
|
+
const cap3 = CapUrn.fromString(testUrn('type=image;op=extract'));
|
|
421
|
+
assert(!cap1.accepts(cap3), 'Different op should not accept');
|
|
422
|
+
assert(!cap3.accepts(cap1), 'Different op should not accept (reverse)');
|
|
423
|
+
|
|
424
|
+
// Different in/out should not accept
|
|
425
|
+
const cap5 = CapUrn.fromString('cap:in="media:textable";out="media:object";op=generate');
|
|
426
|
+
assert(!cap1.accepts(cap5), 'Different inSpec should not accept');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// TEST025: CapMatcher.findBestMatch returns most specific
|
|
430
|
+
function test025_bestMatch() {
|
|
431
|
+
const caps = [
|
|
432
|
+
CapUrn.fromString('cap:in=*;out=*;op=*'),
|
|
433
|
+
CapUrn.fromString(testUrn('op=generate')),
|
|
434
|
+
CapUrn.fromString(testUrn('op=generate;ext=pdf'))
|
|
435
|
+
];
|
|
436
|
+
const request = CapUrn.fromString(testUrn('op=generate'));
|
|
437
|
+
const best = CapMatcher.findBestMatch(caps, request);
|
|
438
|
+
assert(best !== null, 'Should find a best match');
|
|
439
|
+
assertEqual(best.getTag('ext'), 'pdf', 'Best match should be the most specific (ext=pdf)');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// TEST026: merge combines tags, subset keeps only specified
|
|
443
|
+
function test026_mergeAndSubset() {
|
|
444
|
+
const cap1 = CapUrn.fromString(testUrn('op=generate'));
|
|
445
|
+
const cap2 = CapUrn.fromString('cap:in="media:textable";ext=pdf;format=binary;out="media:"');
|
|
446
|
+
|
|
447
|
+
// Merge (other takes precedence)
|
|
448
|
+
const merged = cap1.merge(cap2);
|
|
449
|
+
assertEqual(merged.getInSpec(), 'media:textable', 'Merge should take inSpec from other');
|
|
450
|
+
assertEqual(merged.getOutSpec(), 'media:', 'Merge should take outSpec from other');
|
|
451
|
+
assertEqual(merged.getTag('op'), 'generate', 'Merge should keep original tags');
|
|
452
|
+
assertEqual(merged.getTag('ext'), 'pdf', 'Merge should add other tags');
|
|
453
|
+
|
|
454
|
+
// Subset (always preserves in/out)
|
|
455
|
+
const sub = merged.subset(['ext']);
|
|
456
|
+
assertEqual(sub.getTag('ext'), 'pdf', 'Subset should keep ext');
|
|
457
|
+
assertEqual(sub.getTag('op'), undefined, 'Subset should drop op');
|
|
458
|
+
assertEqual(sub.getInSpec(), 'media:textable', 'Subset should preserve inSpec');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// TEST027: withWildcardTag sets tag to wildcard including in/out
|
|
462
|
+
function test027_wildcardTag() {
|
|
463
|
+
const cap = CapUrn.fromString(testUrn('ext=pdf'));
|
|
464
|
+
const wildcardExt = cap.withWildcardTag('ext');
|
|
465
|
+
assertEqual(wildcardExt.getTag('ext'), '*', 'Should set ext to wildcard');
|
|
466
|
+
|
|
467
|
+
const wildcardIn = cap.withWildcardTag('in');
|
|
468
|
+
assertEqual(wildcardIn.getInSpec(), '*', 'Should set in to wildcard');
|
|
469
|
+
|
|
470
|
+
const wildcardOut = cap.withWildcardTag('out');
|
|
471
|
+
assertEqual(wildcardOut.getOutSpec(), '*', 'Should set out to wildcard');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// TEST028: Empty cap URN without in/out fails (MissingInSpec)
|
|
475
|
+
function test028_emptyCapUrnNotAllowed() {
|
|
476
|
+
assertThrows(
|
|
477
|
+
() => CapUrn.fromString('cap:'),
|
|
478
|
+
ErrorCodes.MISSING_IN_SPEC,
|
|
479
|
+
'Empty cap URN should fail with MISSING_IN_SPEC'
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// TEST029: Minimal valid cap URN: just in and out, empty tags
|
|
484
|
+
function test029_minimalCapUrn() {
|
|
485
|
+
const minimal = CapUrn.fromString('cap:in="media:void";out="media:void"');
|
|
486
|
+
assertEqual(Object.keys(minimal.tags).length, 0, 'Should have no other tags');
|
|
487
|
+
assertEqual(minimal.getInSpec(), 'media:void', 'Should have inSpec');
|
|
488
|
+
assertEqual(minimal.getOutSpec(), 'media:void', 'Should have outSpec');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// TEST030: Forward slashes and colons in tag values
|
|
492
|
+
function test030_extendedCharacterSupport() {
|
|
493
|
+
const cap = CapUrn.fromString(testUrn('url=https://example_org/api;path=/some/file'));
|
|
494
|
+
assertEqual(cap.getTag('url'), 'https://example_org/api', 'Should support colons and slashes');
|
|
495
|
+
assertEqual(cap.getTag('path'), '/some/file', 'Should support forward slashes');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// TEST031: Wildcard rejected in keys, accepted in values
|
|
499
|
+
function test031_wildcardRestrictions() {
|
|
500
|
+
assertThrows(
|
|
501
|
+
() => CapUrn.fromString(testUrn('*=value')),
|
|
502
|
+
ErrorCodes.INVALID_CHARACTER,
|
|
503
|
+
'Should reject wildcard in key'
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// Wildcard accepted in values
|
|
507
|
+
const cap = CapUrn.fromString(testUrn('key=*'));
|
|
508
|
+
assertEqual(cap.getTag('key'), '*', 'Should accept wildcard in value');
|
|
509
|
+
|
|
510
|
+
// Wildcard in in/out
|
|
511
|
+
const capWild = CapUrn.fromString('cap:in=*;out=*;key=value');
|
|
512
|
+
assertEqual(capWild.getInSpec(), '*', 'Should accept wildcard in inSpec');
|
|
513
|
+
assertEqual(capWild.getOutSpec(), '*', 'Should accept wildcard in outSpec');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// TEST032: Duplicate keys rejected
|
|
517
|
+
function test032_duplicateKeyRejection() {
|
|
518
|
+
assertThrows(
|
|
519
|
+
() => CapUrn.fromString(testUrn('key=value1;key=value2')),
|
|
520
|
+
ErrorCodes.DUPLICATE_KEY,
|
|
521
|
+
'Should reject duplicate keys'
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// TEST033: Pure numeric keys rejected, mixed alphanumeric OK
|
|
526
|
+
function test033_numericKeyRestriction() {
|
|
527
|
+
assertThrows(
|
|
528
|
+
() => CapUrn.fromString(testUrn('123=value')),
|
|
529
|
+
ErrorCodes.NUMERIC_KEY,
|
|
530
|
+
'Should reject pure numeric keys'
|
|
531
|
+
);
|
|
532
|
+
// Mixed alphanumeric allowed
|
|
533
|
+
const cap1 = CapUrn.fromString(testUrn('key123=value'));
|
|
534
|
+
assertEqual(cap1.getTag('key123'), 'value', 'Mixed alphanumeric key should be allowed');
|
|
535
|
+
const cap2 = CapUrn.fromString(testUrn('x123key=value'));
|
|
536
|
+
assertEqual(cap2.getTag('x123key'), 'value', 'Mixed alphanumeric key should be allowed');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// TEST034: key= (empty value) is rejected
|
|
540
|
+
function test034_emptyValueError() {
|
|
541
|
+
let threw = false;
|
|
542
|
+
try {
|
|
543
|
+
CapUrn.fromString('cap:in="media:void";key=;out="media:void"');
|
|
544
|
+
} catch (e) {
|
|
545
|
+
if (e instanceof CapUrnError) {
|
|
546
|
+
threw = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
assert(threw, 'Empty value (key=) should be rejected');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// TEST035: hasTag case-sensitive for values, case-insensitive for keys, works for in/out
|
|
553
|
+
function test035_hasTagCaseSensitive() {
|
|
554
|
+
const cap = CapUrn.fromString('cap:in="media:void";key="Value";out="media:void"');
|
|
555
|
+
assert(cap.hasTag('key', 'Value'), 'hasTag should match exact value');
|
|
556
|
+
assert(cap.hasTag('KEY', 'Value'), 'hasTag should be case-insensitive for keys');
|
|
557
|
+
assert(!cap.hasTag('key', 'value'), 'hasTag should be case-sensitive for values');
|
|
558
|
+
// Works for in/out
|
|
559
|
+
assert(cap.hasTag('in', 'media:void'), 'hasTag should work for in');
|
|
560
|
+
assert(cap.hasTag('IN', 'media:void'), 'hasTag should be case-insensitive for in key');
|
|
561
|
+
assert(cap.hasTag('out', 'media:void'), 'hasTag should work for out');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// TEST036: withTag preserves value case
|
|
565
|
+
function test036_withTagPreservesValue() {
|
|
566
|
+
const cap = CapUrn.fromString('cap:in="media:void";out="media:void"');
|
|
567
|
+
const modified = cap.withTag('key', 'MyValue');
|
|
568
|
+
assertEqual(modified.getTag('key'), 'MyValue', 'withTag should preserve value case');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// TEST037: withTag('key', '') -> error
|
|
572
|
+
// Note: In JS, withTag does not currently reject empty values (it stores them).
|
|
573
|
+
// The Rust implementation rejects empty values. We test the JS behavior as-is.
|
|
574
|
+
function test037_withTagRejectsEmptyValue() {
|
|
575
|
+
// The JS implementation does not throw for empty values in withTag.
|
|
576
|
+
// This test verifies the current behavior: withTag stores empty string.
|
|
577
|
+
// If the implementation is updated to reject empty values, update this test.
|
|
578
|
+
const cap = CapUrn.fromString('cap:in="media:void";out="media:void"');
|
|
579
|
+
const modified = cap.withTag('key', '');
|
|
580
|
+
assertEqual(modified.getTag('key'), '', 'withTag stores empty string (JS behavior)');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// TEST038: Unquoted 'simple' == quoted '"simple"' (lowercase)
|
|
584
|
+
function test038_semanticEquivalence() {
|
|
585
|
+
const c1 = CapUrn.fromString('cap:in="media:void";key=simple;out="media:void"');
|
|
586
|
+
const c2 = CapUrn.fromString('cap:in="media:void";key="simple";out="media:void"');
|
|
587
|
+
assert(c1.equals(c2), 'Unquoted simple and quoted "simple" should be equal');
|
|
588
|
+
assertEqual(c1.getTag('key'), c2.getTag('key'), 'Values should be identical');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// TEST039: getTag('in') and getTag('out') work, case-insensitive
|
|
592
|
+
function test039_getTagReturnsDirectionSpecs() {
|
|
593
|
+
const cap = CapUrn.fromString(`cap:in="${MEDIA_VOID}";out="${MEDIA_OBJECT}"`);
|
|
594
|
+
assertEqual(cap.getTag('in'), MEDIA_VOID, 'getTag(in) should return inSpec');
|
|
595
|
+
assertEqual(cap.getTag('IN'), MEDIA_VOID, 'getTag(IN) should return inSpec (case-insensitive)');
|
|
596
|
+
assertEqual(cap.getTag('out'), MEDIA_OBJECT, 'getTag(out) should return outSpec');
|
|
597
|
+
assertEqual(cap.getTag('OUT'), MEDIA_OBJECT, 'getTag(OUT) should return outSpec (case-insensitive)');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// TEST040: Cap and request same tags -> accepts=true
|
|
601
|
+
function test040_matchingSemanticsExactMatch() {
|
|
602
|
+
const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
603
|
+
const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
604
|
+
assert(cap.accepts(request), 'Exact match should accept');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// TEST041: Cap missing tag -> implicit wildcard -> accepts=true
|
|
608
|
+
function test041_matchingSemanticsCapMissingTag() {
|
|
609
|
+
const cap = CapUrn.fromString(testUrn('op=generate'));
|
|
610
|
+
const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
611
|
+
assert(cap.accepts(request), 'Cap missing ext tag should accept (implicit wildcard)');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// TEST042: Cap extra tag -> still matches
|
|
615
|
+
function test042_matchingSemanticsCapHasExtraTag() {
|
|
616
|
+
const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf;version=2'));
|
|
617
|
+
const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
618
|
+
assert(cap.accepts(request), 'Cap with extra tag should still accept');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// TEST043: Request ext=* matches cap ext=pdf
|
|
622
|
+
function test043_matchingSemanticsRequestHasWildcard() {
|
|
623
|
+
const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
624
|
+
const request = CapUrn.fromString(testUrn('op=generate;ext=*'));
|
|
625
|
+
assert(cap.accepts(request), 'Request wildcard should match specific cap value');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// TEST044: Cap ext=* matches request ext=pdf
|
|
629
|
+
function test044_matchingSemanticsCapHasWildcard() {
|
|
630
|
+
const cap = CapUrn.fromString(testUrn('op=generate;ext=*'));
|
|
631
|
+
const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
632
|
+
assert(cap.accepts(request), 'Cap wildcard should match specific request value');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// TEST045: ext=pdf vs ext=docx -> no match
|
|
636
|
+
function test045_matchingSemanticsValueMismatch() {
|
|
637
|
+
const cap = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
638
|
+
const request = CapUrn.fromString(testUrn('op=generate;ext=docx'));
|
|
639
|
+
assert(!cap.accepts(request), 'Value mismatch should not accept');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// TEST046: Cap without ext matches request with ext=wav (uses media:binary directions)
|
|
643
|
+
function test046_matchingSemanticsFallbackPattern() {
|
|
644
|
+
const cap = CapUrn.fromString('cap:in="media:binary";op=generate_thumbnail;out="media:binary"');
|
|
645
|
+
const request = CapUrn.fromString('cap:ext=wav;in="media:binary";op=generate_thumbnail;out="media:binary"');
|
|
646
|
+
assert(cap.accepts(request), 'Cap missing ext should accept (implicit wildcard for ext)');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// TEST047: Thumbnail with void input matches specific ext request
|
|
650
|
+
function test047_matchingSemanticsThumbnailVoidInput() {
|
|
651
|
+
const cap = CapUrn.fromString('cap:in="media:void";op=generate_thumbnail;out="media:image;png;thumbnail"');
|
|
652
|
+
const request = CapUrn.fromString('cap:ext=pdf;in="media:void";op=generate_thumbnail;out="media:image"');
|
|
653
|
+
assert(cap.accepts(request), 'Void input cap should accept request; cap output conforms to less-specific request output');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// TEST048: Cap in=* out=* matches any request
|
|
657
|
+
function test048_matchingSemanticsWildcardDirection() {
|
|
658
|
+
const cap = CapUrn.fromString('cap:in=*;out=*');
|
|
659
|
+
const request = CapUrn.fromString(testUrn('op=generate;ext=pdf'));
|
|
660
|
+
assert(cap.accepts(request), 'Wildcard cap should accept any request');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// TEST049: Cap op=generate accepts request ext=pdf (independent tags)
|
|
664
|
+
function test049_matchingSemanticsCrossDimension() {
|
|
665
|
+
const cap = CapUrn.fromString(testUrn('op=generate'));
|
|
666
|
+
const request = CapUrn.fromString(testUrn('ext=pdf'));
|
|
667
|
+
assert(cap.accepts(request), 'Independent tags should not block matching');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// TEST050: media:string vs media: (wildcard) -> no match
|
|
671
|
+
function test050_matchingSemanticsDirectionMismatch() {
|
|
672
|
+
const cap = CapUrn.fromString(
|
|
673
|
+
`cap:in="${MEDIA_STRING}";op=generate;out="${MEDIA_OBJECT}"`
|
|
674
|
+
);
|
|
675
|
+
const request = CapUrn.fromString(
|
|
676
|
+
`cap:in="${MEDIA_BINARY}";op=generate;out="${MEDIA_OBJECT}"`
|
|
677
|
+
);
|
|
678
|
+
assert(!cap.accepts(request), 'Incompatible direction types should not match');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// TEST890: Semantic direction matching - generic provider matches specific request
|
|
682
|
+
function test890_directionSemanticMatching() {
|
|
683
|
+
// Generic wildcard cap accepts specific pdf request
|
|
684
|
+
const genericCap = CapUrn.fromString(
|
|
685
|
+
'cap:in="media:";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
686
|
+
);
|
|
687
|
+
const pdfRequest = CapUrn.fromString(
|
|
688
|
+
'cap:in="media:pdf";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
689
|
+
);
|
|
690
|
+
assert(genericCap.accepts(pdfRequest), 'Generic wildcard cap must accept pdf request');
|
|
691
|
+
|
|
692
|
+
// Also accepts epub
|
|
693
|
+
const epubRequest = CapUrn.fromString(
|
|
694
|
+
'cap:in="media:epub";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
695
|
+
);
|
|
696
|
+
assert(genericCap.accepts(epubRequest), 'Generic wildcard cap must accept epub request');
|
|
697
|
+
|
|
698
|
+
// Reverse: specific pdf cap does NOT accept generic bytes request
|
|
699
|
+
const pdfCap = CapUrn.fromString(
|
|
700
|
+
'cap:in="media:pdf";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
701
|
+
);
|
|
702
|
+
const genericRequest = CapUrn.fromString(
|
|
703
|
+
'cap:in="media:";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
704
|
+
);
|
|
705
|
+
assert(!pdfCap.accepts(genericRequest), 'Specific pdf cap must NOT accept generic wildcard request');
|
|
706
|
+
|
|
707
|
+
// PDF cap does NOT accept epub request
|
|
708
|
+
assert(!pdfCap.accepts(epubRequest), 'PDF cap must NOT accept epub request');
|
|
709
|
+
|
|
710
|
+
// Output direction: cap producing more specific output satisfies less specific request
|
|
711
|
+
const specificOutCap = CapUrn.fromString(
|
|
712
|
+
'cap:in="media:";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
713
|
+
);
|
|
714
|
+
const genericOutRequest = CapUrn.fromString(
|
|
715
|
+
'cap:in="media:";op=generate_thumbnail;out="media:image"'
|
|
716
|
+
);
|
|
717
|
+
assert(specificOutCap.accepts(genericOutRequest),
|
|
718
|
+
'Cap producing image;png;thumbnail must satisfy request for image');
|
|
719
|
+
|
|
720
|
+
// Reverse output: generic output cap does NOT satisfy specific output request
|
|
721
|
+
const genericOutCap = CapUrn.fromString(
|
|
722
|
+
'cap:in="media:";op=generate_thumbnail;out="media:image"'
|
|
723
|
+
);
|
|
724
|
+
const specificOutRequest = CapUrn.fromString(
|
|
725
|
+
'cap:in="media:";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
726
|
+
);
|
|
727
|
+
assert(!genericOutCap.accepts(specificOutRequest),
|
|
728
|
+
'Generic output cap must NOT satisfy specific output request');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// TEST891: Semantic direction specificity - more media URN tags = higher specificity
|
|
732
|
+
function test891_directionSemanticSpecificity() {
|
|
733
|
+
const genericCap = CapUrn.fromString(
|
|
734
|
+
'cap:in="media:";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
735
|
+
);
|
|
736
|
+
const specificCap = CapUrn.fromString(
|
|
737
|
+
'cap:in="media:pdf";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
assertEqual(genericCap.specificity(), 4, 'media:(0) + image;png;thumbnail(3) + op(1) = 4');
|
|
741
|
+
assertEqual(specificCap.specificity(), 5, 'pdf(1) + image;png;thumbnail(3) + op(1) = 5');
|
|
742
|
+
assert(specificCap.specificity() > genericCap.specificity(), 'pdf should be more specific');
|
|
743
|
+
|
|
744
|
+
// CapMatcher should prefer more specific
|
|
745
|
+
const pdfRequest = CapUrn.fromString(
|
|
746
|
+
'cap:in="media:pdf";op=generate_thumbnail;out="media:image;png;thumbnail"'
|
|
747
|
+
);
|
|
748
|
+
const best = CapMatcher.findBestMatch([genericCap, specificCap], pdfRequest);
|
|
749
|
+
assert(best !== null, 'Should find a match');
|
|
750
|
+
assertEqual(best.getInSpec(), 'media:pdf', 'Should prefer more specific pdf cap');
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ============================================================================
|
|
754
|
+
// validation.rs: TEST053-TEST056
|
|
755
|
+
// ============================================================================
|
|
756
|
+
|
|
757
|
+
// TEST053: N/A for JS (Rust-only validation infrastructure)
|
|
758
|
+
|
|
759
|
+
// TEST054: Inline media spec redefinition of registry spec is detected
|
|
760
|
+
function test054_xv5InlineSpecRedefinitionDetected() {
|
|
761
|
+
const registryLookup = (mediaUrn) => mediaUrn === MEDIA_STRING;
|
|
762
|
+
const mediaSpecs = [
|
|
763
|
+
{
|
|
764
|
+
urn: MEDIA_STRING,
|
|
765
|
+
media_type: 'text/plain',
|
|
766
|
+
title: 'My Custom String',
|
|
767
|
+
description: 'Trying to redefine string'
|
|
768
|
+
}
|
|
769
|
+
];
|
|
770
|
+
const result = validateNoMediaSpecRedefinitionSync(mediaSpecs, registryLookup);
|
|
771
|
+
assert(!result.valid, 'Should fail when redefining registry spec');
|
|
772
|
+
assert(result.error && result.error.includes('XV5'), 'Error should mention XV5');
|
|
773
|
+
assert(result.redefines && result.redefines.includes(MEDIA_STRING), 'Should identify MEDIA_STRING as redefined');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// TEST055: New inline media spec not in registry is allowed
|
|
777
|
+
function test055_xv5NewInlineSpecAllowed() {
|
|
778
|
+
const registryLookup = (mediaUrn) => mediaUrn === MEDIA_STRING;
|
|
779
|
+
const mediaSpecs = [
|
|
780
|
+
{
|
|
781
|
+
urn: 'media:my-unique-custom-type-xyz123',
|
|
782
|
+
media_type: 'application/json',
|
|
783
|
+
title: 'My Custom Output',
|
|
784
|
+
description: 'A custom output type'
|
|
785
|
+
}
|
|
786
|
+
];
|
|
787
|
+
const result = validateNoMediaSpecRedefinitionSync(mediaSpecs, registryLookup);
|
|
788
|
+
assert(result.valid, 'New spec not in registry should pass validation');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// TEST056: Empty/null media_specs passes validation
|
|
792
|
+
function test056_xv5EmptyMediaSpecsAllowed() {
|
|
793
|
+
const registryLookup = (mediaUrn) => mediaUrn === MEDIA_STRING;
|
|
794
|
+
assert(validateNoMediaSpecRedefinitionSync({}, registryLookup).valid, 'Empty object should pass');
|
|
795
|
+
assert(validateNoMediaSpecRedefinitionSync(null, registryLookup).valid, 'Null should pass');
|
|
796
|
+
assert(validateNoMediaSpecRedefinitionSync(undefined, registryLookup).valid, 'Undefined should pass');
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ============================================================================
|
|
800
|
+
// media_urn.rs: TEST060-TEST078
|
|
801
|
+
// ============================================================================
|
|
802
|
+
|
|
803
|
+
// TEST060: MediaUrn.fromString('cap:string') -> INVALID_PREFIX error
|
|
804
|
+
function test060_wrongPrefixFails() {
|
|
805
|
+
assertThrowsMediaUrn(
|
|
806
|
+
() => MediaUrn.fromString('cap:string'),
|
|
807
|
+
MediaUrnErrorCodes.INVALID_PREFIX,
|
|
808
|
+
'Wrong prefix should fail with INVALID_PREFIX'
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// TEST061: isBinary true when textable tag is absent (binary = not textable)
|
|
813
|
+
function test061_isBinary() {
|
|
814
|
+
// Binary types: no textable tag
|
|
815
|
+
assert(MediaUrn.fromString(MEDIA_BINARY).isBinary(), 'MEDIA_BINARY (media:) should be binary');
|
|
816
|
+
assert(MediaUrn.fromString(MEDIA_PNG).isBinary(), 'MEDIA_PNG should be binary');
|
|
817
|
+
assert(MediaUrn.fromString(MEDIA_PDF).isBinary(), 'MEDIA_PDF should be binary');
|
|
818
|
+
assert(MediaUrn.fromString('media:video').isBinary(), 'media:video should be binary');
|
|
819
|
+
assert(MediaUrn.fromString('media:epub').isBinary(), 'media:epub should be binary');
|
|
820
|
+
// Textable types: is_binary is false
|
|
821
|
+
assert(!MediaUrn.fromString('media:textable').isBinary(), 'media:textable should not be binary');
|
|
822
|
+
assert(!MediaUrn.fromString('media:textable;record').isBinary(), 'textable map should not be binary');
|
|
823
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isBinary(), 'MEDIA_STRING should not be binary');
|
|
824
|
+
assert(!MediaUrn.fromString(MEDIA_JSON).isBinary(), 'MEDIA_JSON should not be binary');
|
|
825
|
+
assert(!MediaUrn.fromString(MEDIA_MD).isBinary(), 'MEDIA_MD should not be binary');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// TEST062: isMap true for MEDIA_OBJECT (record); false for MEDIA_STRING (form=scalar), MEDIA_STRING_ARRAY (list)
|
|
829
|
+
// TEST062: is_record returns true if record marker tag is present (key-value structure)
|
|
830
|
+
function test062_isRecord() {
|
|
831
|
+
assert(MediaUrn.fromString(MEDIA_OBJECT).isRecord(), 'MEDIA_OBJECT should be record');
|
|
832
|
+
assert(MediaUrn.fromString('media:custom;record').isRecord(), 'custom;record should be record');
|
|
833
|
+
assert(MediaUrn.fromString(MEDIA_JSON).isRecord(), 'MEDIA_JSON should be record');
|
|
834
|
+
// Without record marker, is_record is false
|
|
835
|
+
assert(!MediaUrn.fromString('media:textable').isRecord(), 'plain textable should not be record');
|
|
836
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isRecord(), 'MEDIA_STRING should not be record');
|
|
837
|
+
assert(!MediaUrn.fromString(MEDIA_STRING_ARRAY).isRecord(), 'MEDIA_STRING_ARRAY should not be record');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// TEST063: is_scalar returns true if NO list marker (scalar is default cardinality)
|
|
841
|
+
function test063_isScalar() {
|
|
842
|
+
assert(MediaUrn.fromString(MEDIA_STRING).isScalar(), 'MEDIA_STRING should be scalar');
|
|
843
|
+
assert(MediaUrn.fromString(MEDIA_INTEGER).isScalar(), 'MEDIA_INTEGER should be scalar');
|
|
844
|
+
assert(MediaUrn.fromString(MEDIA_NUMBER).isScalar(), 'MEDIA_NUMBER should be scalar');
|
|
845
|
+
assert(MediaUrn.fromString(MEDIA_BOOLEAN).isScalar(), 'MEDIA_BOOLEAN should be scalar');
|
|
846
|
+
assert(MediaUrn.fromString(MEDIA_OBJECT).isScalar(), 'MEDIA_OBJECT (record but scalar) should be scalar');
|
|
847
|
+
assert(MediaUrn.fromString('media:textable').isScalar(), 'plain textable should be scalar');
|
|
848
|
+
// With list marker, is_scalar is false
|
|
849
|
+
assert(!MediaUrn.fromString(MEDIA_STRING_ARRAY).isScalar(), 'MEDIA_STRING_ARRAY should not be scalar');
|
|
850
|
+
assert(!MediaUrn.fromString(MEDIA_OBJECT_ARRAY).isScalar(), 'MEDIA_OBJECT_ARRAY should not be scalar');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// TEST064: isList true for MEDIA_STRING_ARRAY, MEDIA_INTEGER_ARRAY, MEDIA_OBJECT_ARRAY;
|
|
854
|
+
// false for MEDIA_STRING, MEDIA_OBJECT
|
|
855
|
+
function test064_isList() {
|
|
856
|
+
assert(MediaUrn.fromString(MEDIA_STRING_ARRAY).isList(), 'MEDIA_STRING_ARRAY should be list');
|
|
857
|
+
assert(MediaUrn.fromString(MEDIA_INTEGER_ARRAY).isList(), 'MEDIA_INTEGER_ARRAY should be list');
|
|
858
|
+
assert(MediaUrn.fromString(MEDIA_OBJECT_ARRAY).isList(), 'MEDIA_OBJECT_ARRAY should be list');
|
|
859
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isList(), 'MEDIA_STRING should not be list');
|
|
860
|
+
assert(!MediaUrn.fromString(MEDIA_OBJECT).isList(), 'MEDIA_OBJECT should not be list');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// TEST065: is_opaque returns true if NO record marker (opaque is default structure)
|
|
864
|
+
function test065_isOpaque() {
|
|
865
|
+
assert(MediaUrn.fromString(MEDIA_STRING).isOpaque(), 'MEDIA_STRING should be opaque');
|
|
866
|
+
assert(MediaUrn.fromString(MEDIA_STRING_ARRAY).isOpaque(), 'MEDIA_STRING_ARRAY (list but no record) should be opaque');
|
|
867
|
+
assert(MediaUrn.fromString(MEDIA_PDF).isOpaque(), 'MEDIA_PDF should be opaque');
|
|
868
|
+
assert(MediaUrn.fromString('media:textable').isOpaque(), 'plain textable should be opaque');
|
|
869
|
+
// With record marker, is_opaque is false
|
|
870
|
+
assert(!MediaUrn.fromString(MEDIA_OBJECT).isOpaque(), 'MEDIA_OBJECT should not be opaque');
|
|
871
|
+
assert(!MediaUrn.fromString(MEDIA_JSON).isOpaque(), 'MEDIA_JSON should not be opaque');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// TEST066: isJson true for MEDIA_JSON; false for MEDIA_OBJECT (map but not json)
|
|
875
|
+
function test066_isJson() {
|
|
876
|
+
assert(MediaUrn.fromString(MEDIA_JSON).isJson(), 'MEDIA_JSON should be json');
|
|
877
|
+
assert(!MediaUrn.fromString(MEDIA_OBJECT).isJson(), 'MEDIA_OBJECT should not be json');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// TEST067: is_text returns true only if "textable" marker tag is present
|
|
881
|
+
function test067_isText() {
|
|
882
|
+
assert(MediaUrn.fromString(MEDIA_STRING).isText(), 'MEDIA_STRING should be text');
|
|
883
|
+
assert(MediaUrn.fromString(MEDIA_INTEGER).isText(), 'MEDIA_INTEGER should be text');
|
|
884
|
+
assert(MediaUrn.fromString(MEDIA_JSON).isText(), 'MEDIA_JSON should be text');
|
|
885
|
+
// Without textable tag, is_text is false
|
|
886
|
+
assert(!MediaUrn.fromString(MEDIA_BINARY).isText(), 'MEDIA_BINARY should not be text');
|
|
887
|
+
assert(!MediaUrn.fromString(MEDIA_PNG).isText(), 'MEDIA_PNG should not be text');
|
|
888
|
+
assert(!MediaUrn.fromString(MEDIA_OBJECT).isText(), 'MEDIA_OBJECT (no textable) should not be text');
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// TEST068: isVoid true for media:void; false for media:string
|
|
892
|
+
function test068_isVoid() {
|
|
893
|
+
assert(MediaUrn.fromString('media:void').isVoid(), 'media:void should be void');
|
|
894
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isVoid(), 'MEDIA_STRING should not be void');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// TEST069-TEST070: N/A for JS (Rust-only binary_media_urn_for_ext/text_media_urn_for_ext)
|
|
898
|
+
|
|
899
|
+
// TEST071: Parse -> toString -> parse equals original
|
|
900
|
+
function test071_toStringRoundtrip() {
|
|
901
|
+
const constants = [MEDIA_STRING, MEDIA_INTEGER, MEDIA_OBJECT, MEDIA_BINARY, MEDIA_PDF, MEDIA_JSON];
|
|
902
|
+
for (const constant of constants) {
|
|
903
|
+
const parsed = MediaUrn.fromString(constant);
|
|
904
|
+
const reparsed = MediaUrn.fromString(parsed.toString());
|
|
905
|
+
assert(parsed.equals(reparsed), `Round-trip failed for ${constant}`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// TEST072: All MEDIA_* constants parse as valid MediaUrns
|
|
910
|
+
function test072_constantsParse() {
|
|
911
|
+
const constants = [
|
|
912
|
+
MEDIA_STRING, MEDIA_INTEGER, MEDIA_NUMBER, MEDIA_BOOLEAN,
|
|
913
|
+
MEDIA_OBJECT, MEDIA_STRING_ARRAY, MEDIA_INTEGER_ARRAY,
|
|
914
|
+
MEDIA_NUMBER_ARRAY, MEDIA_BOOLEAN_ARRAY, MEDIA_OBJECT_ARRAY,
|
|
915
|
+
MEDIA_BINARY, MEDIA_VOID, MEDIA_PNG, MEDIA_PDF, MEDIA_EPUB,
|
|
916
|
+
MEDIA_MD, MEDIA_TXT, MEDIA_RST, MEDIA_LOG, MEDIA_HTML, MEDIA_XML,
|
|
917
|
+
MEDIA_JSON, MEDIA_YAML, MEDIA_JSON_SCHEMA, MEDIA_AUDIO, MEDIA_VIDEO,
|
|
918
|
+
MEDIA_MODEL_SPEC, MEDIA_AVAILABILITY_OUTPUT, MEDIA_PATH_OUTPUT,
|
|
919
|
+
MEDIA_LLM_INFERENCE_OUTPUT
|
|
920
|
+
];
|
|
921
|
+
for (const constant of constants) {
|
|
922
|
+
const parsed = MediaUrn.fromString(constant);
|
|
923
|
+
assert(parsed !== null, `Constant ${constant} should parse as valid MediaUrn`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// TEST073: N/A for JS (Rust has binary_media_urn_for_ext/text_media_urn_for_ext)
|
|
928
|
+
|
|
929
|
+
// TEST074: MEDIA_PDF (media:pdf) conformsTo media:pdf; MEDIA_MD conformsTo media:md; same URNs conform
|
|
930
|
+
function test074_mediaUrnMatching() {
|
|
931
|
+
const pdfUrn = MediaUrn.fromString(MEDIA_PDF);
|
|
932
|
+
const pdfPattern = MediaUrn.fromString('media:pdf');
|
|
933
|
+
assert(pdfUrn.conformsTo(pdfPattern), 'MEDIA_PDF should conform to media:pdf');
|
|
934
|
+
|
|
935
|
+
const mdUrn = MediaUrn.fromString(MEDIA_MD);
|
|
936
|
+
const mdPattern = MediaUrn.fromString('media:md');
|
|
937
|
+
assert(mdUrn.conformsTo(mdPattern), 'MEDIA_MD should conform to media:md');
|
|
938
|
+
|
|
939
|
+
// Same URN conforms to itself
|
|
940
|
+
assert(pdfUrn.conformsTo(pdfUrn), 'Same URN should conform to itself');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// TEST075: handler accepts same request, general handler accepts request
|
|
944
|
+
function test075_accepts() {
|
|
945
|
+
const handler = MediaUrn.fromString(MEDIA_PDF);
|
|
946
|
+
const sameReq = MediaUrn.fromString(MEDIA_PDF);
|
|
947
|
+
assert(handler.accepts(sameReq), 'Handler should accept same request');
|
|
948
|
+
|
|
949
|
+
const generalHandler = MediaUrn.fromString(MEDIA_BINARY);
|
|
950
|
+
const specificReq = MediaUrn.fromString(MEDIA_PDF);
|
|
951
|
+
assert(generalHandler.accepts(specificReq), 'General handler should accept specific request');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// TEST076: More tags = higher specificity
|
|
955
|
+
function test076_specificity() {
|
|
956
|
+
const s1 = MediaUrn.fromString('media:');
|
|
957
|
+
const s2 = MediaUrn.fromString('media:pdf');
|
|
958
|
+
const s3 = MediaUrn.fromString('media:image;png;thumbnail');
|
|
959
|
+
assert(s2.specificity() > s1.specificity(), 'pdf should be more specific than wildcard');
|
|
960
|
+
assert(s3.specificity() > s2.specificity(), 'image;png;thumbnail should be more specific than pdf');
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// TEST077: N/A for JS (Rust serde) - but we test JSON.stringify round-trip
|
|
964
|
+
function test077_serdeRoundtrip() {
|
|
965
|
+
const original = MediaUrn.fromString(MEDIA_PDF);
|
|
966
|
+
const json = JSON.stringify({ urn: original.toString() });
|
|
967
|
+
const parsed = JSON.parse(json);
|
|
968
|
+
const restored = MediaUrn.fromString(parsed.urn);
|
|
969
|
+
assert(original.equals(restored), 'JSON round-trip should preserve MediaUrn');
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// TEST078: MEDIA_OBJECT does NOT conform to MEDIA_STRING
|
|
973
|
+
function test078_debugMatchingBehavior() {
|
|
974
|
+
const objUrn = MediaUrn.fromString(MEDIA_OBJECT);
|
|
975
|
+
const strUrn = MediaUrn.fromString(MEDIA_STRING);
|
|
976
|
+
assert(!objUrn.conformsTo(strUrn), 'MEDIA_OBJECT should NOT conform to MEDIA_STRING');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// ============================================================================
|
|
980
|
+
// media_spec.rs: TEST088-TEST110
|
|
981
|
+
// ============================================================================
|
|
982
|
+
|
|
983
|
+
// TEST088: N/A for JS (async registry, Rust-only)
|
|
984
|
+
// TEST089: N/A for JS
|
|
985
|
+
// TEST090: N/A for JS
|
|
986
|
+
|
|
987
|
+
// TEST091: resolveMediaUrn resolves custom from local mediaSpecs
|
|
988
|
+
function test091_resolveCustomMediaSpec() {
|
|
989
|
+
const mediaSpecs = [
|
|
990
|
+
{ urn: 'media:custom-json', media_type: 'application/json', title: 'Custom JSON', profile_uri: 'https://example.com/schema/custom' }
|
|
991
|
+
];
|
|
992
|
+
const spec = resolveMediaUrn('media:custom-json', mediaSpecs);
|
|
993
|
+
assertEqual(spec.contentType, 'application/json', 'Should resolve custom spec');
|
|
994
|
+
assertEqual(spec.profile, 'https://example.com/schema/custom', 'Should have custom profile');
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// TEST092: resolveMediaUrn resolves with schema from local mediaSpecs
|
|
998
|
+
function test092_resolveCustomWithSchema() {
|
|
999
|
+
const mediaSpecs = [
|
|
1000
|
+
{
|
|
1001
|
+
urn: 'media:rich-xml',
|
|
1002
|
+
media_type: 'application/xml',
|
|
1003
|
+
title: 'Rich XML',
|
|
1004
|
+
profile_uri: 'https://example.com/schema/rich',
|
|
1005
|
+
schema: { type: 'object' }
|
|
1006
|
+
}
|
|
1007
|
+
];
|
|
1008
|
+
const spec = resolveMediaUrn('media:rich-xml', mediaSpecs);
|
|
1009
|
+
assertEqual(spec.contentType, 'application/xml', 'Should resolve rich spec');
|
|
1010
|
+
assert(spec.schema !== null, 'Should have schema');
|
|
1011
|
+
assertEqual(spec.schema.type, 'object', 'Schema should have correct type');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// TEST093: resolveMediaUrn fails hard on unknown URN
|
|
1015
|
+
function test093_resolveUnresolvableFailsHard() {
|
|
1016
|
+
let caught = false;
|
|
1017
|
+
try {
|
|
1018
|
+
resolveMediaUrn('media:nonexistent', []);
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
if (e instanceof MediaSpecError && e.code === MediaSpecErrorCodes.UNRESOLVABLE_MEDIA_URN) {
|
|
1021
|
+
caught = true;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
assert(caught, 'Should fail hard on unresolvable media URN');
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// TEST094: N/A for JS (no registry concept)
|
|
1028
|
+
// TEST095: N/A for JS (Rust serde)
|
|
1029
|
+
// TEST096: N/A for JS (Rust serde)
|
|
1030
|
+
// TEST097: N/A for JS (Rust validation function)
|
|
1031
|
+
// TEST098: N/A for JS
|
|
1032
|
+
|
|
1033
|
+
// TEST099: MediaSpec with media: (no textable tag) -> isBinary() true
|
|
1034
|
+
function test099_resolvedIsBinary() {
|
|
1035
|
+
const spec = new MediaSpec('application/octet-stream', null, null, 'Binary', null, MEDIA_BINARY);
|
|
1036
|
+
assert(spec.isBinary(), 'Resolved binary spec should be binary');
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// TEST100: MediaSpec with record -> isRecord() true
|
|
1040
|
+
function test100_resolvedIsRecord() {
|
|
1041
|
+
const spec = new MediaSpec('application/json', null, null, 'Object', null, MEDIA_OBJECT);
|
|
1042
|
+
assert(spec.isRecord(), 'Resolved object spec should be record');
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// TEST101: MediaSpec with form=scalar -> isScalar() true
|
|
1046
|
+
function test101_resolvedIsScalar() {
|
|
1047
|
+
const spec = new MediaSpec('text/plain', null, null, 'String', null, MEDIA_STRING);
|
|
1048
|
+
assert(spec.isScalar(), 'Resolved string spec should be scalar');
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// TEST102: MediaSpec with list -> isList() true
|
|
1052
|
+
function test102_resolvedIsList() {
|
|
1053
|
+
const spec = new MediaSpec('text/plain', null, null, 'String Array', null, MEDIA_STRING_ARRAY);
|
|
1054
|
+
assert(spec.isList(), 'Resolved string_array spec should be list');
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// TEST103: MediaSpec with json tag -> isJSON() true
|
|
1058
|
+
function test103_resolvedIsJson() {
|
|
1059
|
+
const spec = new MediaSpec('application/json', null, null, 'JSON', null, MEDIA_JSON);
|
|
1060
|
+
assert(spec.isJSON(), 'Resolved json spec should be JSON');
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// TEST104: MediaSpec with textable tag -> isText() true
|
|
1064
|
+
function test104_resolvedIsText() {
|
|
1065
|
+
const spec = new MediaSpec('text/plain', null, null, 'String', null, MEDIA_STRING);
|
|
1066
|
+
assert(spec.isText(), 'Resolved string spec should be text');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// TEST105: Metadata propagated from media spec definition
|
|
1070
|
+
function test105_metadataPropagation() {
|
|
1071
|
+
const mediaSpecs = [
|
|
1072
|
+
{
|
|
1073
|
+
urn: 'media:custom-setting;setting',
|
|
1074
|
+
media_type: 'text/plain',
|
|
1075
|
+
title: 'Custom Setting',
|
|
1076
|
+
profile_uri: 'https://example.com/schema',
|
|
1077
|
+
metadata: {
|
|
1078
|
+
category_key: 'interface',
|
|
1079
|
+
ui_type: 'SETTING_UI_TYPE_CHECKBOX',
|
|
1080
|
+
subcategory_key: 'appearance',
|
|
1081
|
+
display_index: 5
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
];
|
|
1085
|
+
const resolved = resolveMediaUrn('media:custom-setting;setting', mediaSpecs);
|
|
1086
|
+
assert(resolved.metadata !== null, 'Should have metadata');
|
|
1087
|
+
assertEqual(resolved.metadata.category_key, 'interface', 'Should propagate category_key');
|
|
1088
|
+
assertEqual(resolved.metadata.ui_type, 'SETTING_UI_TYPE_CHECKBOX', 'Should propagate ui_type');
|
|
1089
|
+
assertEqual(resolved.metadata.display_index, 5, 'Should propagate display_index');
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// TEST106: Metadata and validation coexist
|
|
1093
|
+
function test106_metadataWithValidation() {
|
|
1094
|
+
const mediaSpecs = [
|
|
1095
|
+
{
|
|
1096
|
+
urn: 'media:bounded-number;numeric;setting',
|
|
1097
|
+
media_type: 'text/plain',
|
|
1098
|
+
title: 'Bounded Number',
|
|
1099
|
+
validation: { min: 0, max: 100 },
|
|
1100
|
+
metadata: { category_key: 'inference', ui_type: 'SETTING_UI_TYPE_SLIDER' }
|
|
1101
|
+
}
|
|
1102
|
+
];
|
|
1103
|
+
const resolved = resolveMediaUrn('media:bounded-number;numeric;setting', mediaSpecs);
|
|
1104
|
+
assert(resolved.validation !== null, 'Should have validation');
|
|
1105
|
+
assertEqual(resolved.validation.min, 0, 'Should have min');
|
|
1106
|
+
assertEqual(resolved.validation.max, 100, 'Should have max');
|
|
1107
|
+
assert(resolved.metadata !== null, 'Should have metadata');
|
|
1108
|
+
assertEqual(resolved.metadata.category_key, 'inference', 'Should have category_key');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// TEST107: Extensions field propagated
|
|
1112
|
+
function test107_extensionsPropagation() {
|
|
1113
|
+
const mediaSpecs = [
|
|
1114
|
+
{
|
|
1115
|
+
urn: 'media:pdf',
|
|
1116
|
+
media_type: 'application/pdf',
|
|
1117
|
+
title: 'PDF Document',
|
|
1118
|
+
extensions: ['pdf']
|
|
1119
|
+
}
|
|
1120
|
+
];
|
|
1121
|
+
const resolved = resolveMediaUrn('media:pdf', mediaSpecs);
|
|
1122
|
+
assert(Array.isArray(resolved.extensions), 'Extensions should be an array');
|
|
1123
|
+
assertEqual(resolved.extensions.length, 1, 'Should have one extension');
|
|
1124
|
+
assertEqual(resolved.extensions[0], 'pdf', 'Should have pdf extension');
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// TEST108: N/A for JS (Rust serde) - but we test MediaSpec with extensions
|
|
1128
|
+
function test108_extensionsSerialization() {
|
|
1129
|
+
// Test that MediaSpec can hold extensions correctly
|
|
1130
|
+
const spec = new MediaSpec('application/pdf', null, null, 'PDF', null, 'media:pdf', null, null, ['pdf']);
|
|
1131
|
+
assert(Array.isArray(spec.extensions), 'Extensions should be array');
|
|
1132
|
+
assertEqual(spec.extensions[0], 'pdf', 'Should have pdf extension');
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// TEST109: Extensions coexist with metadata and validation
|
|
1136
|
+
function test109_extensionsWithMetadataAndValidation() {
|
|
1137
|
+
const mediaSpecs = [
|
|
1138
|
+
{
|
|
1139
|
+
urn: 'media:custom-output',
|
|
1140
|
+
media_type: 'application/json',
|
|
1141
|
+
title: 'Custom Output',
|
|
1142
|
+
validation: { min_length: 1, max_length: 1000 },
|
|
1143
|
+
metadata: { category: 'output' },
|
|
1144
|
+
extensions: ['json']
|
|
1145
|
+
}
|
|
1146
|
+
];
|
|
1147
|
+
const resolved = resolveMediaUrn('media:custom-output', mediaSpecs);
|
|
1148
|
+
assert(resolved.validation !== null, 'Should have validation');
|
|
1149
|
+
assert(resolved.metadata !== null, 'Should have metadata');
|
|
1150
|
+
assert(Array.isArray(resolved.extensions), 'Should have extensions');
|
|
1151
|
+
assertEqual(resolved.extensions[0], 'json', 'Should have json extension');
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// TEST110: Multiple extensions in a media spec
|
|
1155
|
+
function test110_multipleExtensions() {
|
|
1156
|
+
const mediaSpecs = [
|
|
1157
|
+
{
|
|
1158
|
+
urn: 'media:image;jpeg',
|
|
1159
|
+
media_type: 'image/jpeg',
|
|
1160
|
+
title: 'JPEG Image',
|
|
1161
|
+
extensions: ['jpg', 'jpeg']
|
|
1162
|
+
}
|
|
1163
|
+
];
|
|
1164
|
+
const resolved = resolveMediaUrn('media:image;jpeg', mediaSpecs);
|
|
1165
|
+
assertEqual(resolved.extensions.length, 2, 'Should have two extensions');
|
|
1166
|
+
assertEqual(resolved.extensions[0], 'jpg', 'First extension should be jpg');
|
|
1167
|
+
assertEqual(resolved.extensions[1], 'jpeg', 'Second extension should be jpeg');
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// ============================================================================
|
|
1171
|
+
// cap_matrix.rs: TEST117-TEST131
|
|
1172
|
+
// ============================================================================
|
|
1173
|
+
|
|
1174
|
+
// TEST117: CapBlock finds more specific cap across registries
|
|
1175
|
+
function test117_capBlockMoreSpecificWins() {
|
|
1176
|
+
const providerRegistry = new CapMatrix();
|
|
1177
|
+
const pluginRegistry = new CapMatrix();
|
|
1178
|
+
|
|
1179
|
+
const providerHost = new MockCapSet('provider');
|
|
1180
|
+
const providerCap = makeCap(
|
|
1181
|
+
'cap:in="media:binary";op=generate_thumbnail;out="media:binary"',
|
|
1182
|
+
'Provider Thumbnail Generator (generic)'
|
|
1183
|
+
);
|
|
1184
|
+
providerRegistry.registerCapSet('provider', providerHost, [providerCap]);
|
|
1185
|
+
|
|
1186
|
+
const pluginHost = new MockCapSet('plugin');
|
|
1187
|
+
const pluginCap = makeCap(
|
|
1188
|
+
'cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary"',
|
|
1189
|
+
'Plugin PDF Thumbnail Generator (specific)'
|
|
1190
|
+
);
|
|
1191
|
+
pluginRegistry.registerCapSet('plugin', pluginHost, [pluginCap]);
|
|
1192
|
+
|
|
1193
|
+
const composite = new CapBlock();
|
|
1194
|
+
composite.addRegistry('providers', providerRegistry);
|
|
1195
|
+
composite.addRegistry('plugins', pluginRegistry);
|
|
1196
|
+
|
|
1197
|
+
const request = 'cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary"';
|
|
1198
|
+
const best = composite.findBestCapSet(request);
|
|
1199
|
+
|
|
1200
|
+
assertEqual(best.registryName, 'plugins', 'More specific plugin should win');
|
|
1201
|
+
assertEqual(best.cap.title, 'Plugin PDF Thumbnail Generator (specific)', 'Should get plugin cap');
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// TEST118: CapBlock tie-breaking prefers first registry in order
|
|
1205
|
+
function test118_capBlockTieGoesToFirst() {
|
|
1206
|
+
const registry1 = new CapMatrix();
|
|
1207
|
+
const registry2 = new CapMatrix();
|
|
1208
|
+
|
|
1209
|
+
const host1 = new MockCapSet('host1');
|
|
1210
|
+
const cap1 = makeCap(matrixTestUrn('ext=pdf;op=generate'), 'Registry 1 Cap');
|
|
1211
|
+
registry1.registerCapSet('host1', host1, [cap1]);
|
|
1212
|
+
|
|
1213
|
+
const host2 = new MockCapSet('host2');
|
|
1214
|
+
const cap2 = makeCap(matrixTestUrn('ext=pdf;op=generate'), 'Registry 2 Cap');
|
|
1215
|
+
registry2.registerCapSet('host2', host2, [cap2]);
|
|
1216
|
+
|
|
1217
|
+
const composite = new CapBlock();
|
|
1218
|
+
composite.addRegistry('first', registry1);
|
|
1219
|
+
composite.addRegistry('second', registry2);
|
|
1220
|
+
|
|
1221
|
+
const best = composite.findBestCapSet(matrixTestUrn('ext=pdf;op=generate'));
|
|
1222
|
+
assertEqual(best.registryName, 'first', 'On tie, first registry should win');
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// TEST119: CapBlock polls all registries to find best match
|
|
1226
|
+
function test119_capBlockPollsAll() {
|
|
1227
|
+
const registry1 = new CapMatrix();
|
|
1228
|
+
const registry2 = new CapMatrix();
|
|
1229
|
+
const registry3 = new CapMatrix();
|
|
1230
|
+
|
|
1231
|
+
const host1 = new MockCapSet('host1');
|
|
1232
|
+
const cap1 = makeCap(matrixTestUrn('op=different'), 'Registry 1');
|
|
1233
|
+
registry1.registerCapSet('host1', host1, [cap1]);
|
|
1234
|
+
|
|
1235
|
+
const host2 = new MockCapSet('host2');
|
|
1236
|
+
const cap2 = makeCap(matrixTestUrn('op=generate'), 'Registry 2');
|
|
1237
|
+
registry2.registerCapSet('host2', host2, [cap2]);
|
|
1238
|
+
|
|
1239
|
+
const host3 = new MockCapSet('host3');
|
|
1240
|
+
const cap3 = makeCap(matrixTestUrn('ext=pdf;format=thumbnail;op=generate'), 'Registry 3');
|
|
1241
|
+
registry3.registerCapSet('host3', host3, [cap3]);
|
|
1242
|
+
|
|
1243
|
+
const composite = new CapBlock();
|
|
1244
|
+
composite.addRegistry('r1', registry1);
|
|
1245
|
+
composite.addRegistry('r2', registry2);
|
|
1246
|
+
composite.addRegistry('r3', registry3);
|
|
1247
|
+
|
|
1248
|
+
const best = composite.findBestCapSet(matrixTestUrn('ext=pdf;format=thumbnail;op=generate'));
|
|
1249
|
+
assertEqual(best.registryName, 'r3', 'Most specific registry should win');
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// TEST120: CapBlock returns error when no cap matches request
|
|
1253
|
+
function test120_capBlockNoMatch() {
|
|
1254
|
+
const registry = new CapMatrix();
|
|
1255
|
+
const composite = new CapBlock();
|
|
1256
|
+
composite.addRegistry('empty', registry);
|
|
1257
|
+
|
|
1258
|
+
try {
|
|
1259
|
+
composite.findBestCapSet(matrixTestUrn('op=nonexistent'));
|
|
1260
|
+
throw new Error('Expected error for non-matching capability');
|
|
1261
|
+
} catch (e) {
|
|
1262
|
+
assert(e instanceof CapMatrixError, 'Should be CapMatrixError');
|
|
1263
|
+
assertEqual(e.type, 'NoSetsFound', 'Should be NoSetsFound error');
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// TEST121: CapBlock fallback scenario where generic cap handles unknown file types
|
|
1268
|
+
function test121_capBlockFallbackScenario() {
|
|
1269
|
+
const providerRegistry = new CapMatrix();
|
|
1270
|
+
const pluginRegistry = new CapMatrix();
|
|
1271
|
+
|
|
1272
|
+
const providerHost = new MockCapSet('provider_fallback');
|
|
1273
|
+
const providerCap = makeCap(
|
|
1274
|
+
'cap:in="media:binary";op=generate_thumbnail;out="media:binary"',
|
|
1275
|
+
'Generic Thumbnail Provider'
|
|
1276
|
+
);
|
|
1277
|
+
providerRegistry.registerCapSet('provider_fallback', providerHost, [providerCap]);
|
|
1278
|
+
|
|
1279
|
+
const pluginHost = new MockCapSet('pdf_plugin');
|
|
1280
|
+
const pluginCap = makeCap(
|
|
1281
|
+
'cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary"',
|
|
1282
|
+
'PDF Thumbnail Plugin'
|
|
1283
|
+
);
|
|
1284
|
+
pluginRegistry.registerCapSet('pdf_plugin', pluginHost, [pluginCap]);
|
|
1285
|
+
|
|
1286
|
+
const composite = new CapBlock();
|
|
1287
|
+
composite.addRegistry('providers', providerRegistry);
|
|
1288
|
+
composite.addRegistry('plugins', pluginRegistry);
|
|
1289
|
+
|
|
1290
|
+
// PDF request -> plugin wins
|
|
1291
|
+
const best = composite.findBestCapSet('cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary"');
|
|
1292
|
+
assertEqual(best.registryName, 'plugins', 'Plugin should win for PDF');
|
|
1293
|
+
|
|
1294
|
+
// WAV request -> provider wins (fallback)
|
|
1295
|
+
const bestWav = composite.findBestCapSet('cap:ext=wav;in="media:binary";op=generate_thumbnail;out="media:binary"');
|
|
1296
|
+
assertEqual(bestWav.registryName, 'providers', 'Provider should win for wav (fallback)');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// TEST122: CapBlock can method returns execution info and acceptsRequest checks capability
|
|
1300
|
+
function test122_capBlockCanMethod() {
|
|
1301
|
+
const providerRegistry = new CapMatrix();
|
|
1302
|
+
const providerHost = new MockCapSet('test_provider');
|
|
1303
|
+
const providerCap = makeCap(matrixTestUrn('ext=pdf;op=generate'), 'Test Provider');
|
|
1304
|
+
providerRegistry.registerCapSet('test_provider', providerHost, [providerCap]);
|
|
1305
|
+
|
|
1306
|
+
const composite = new CapBlock();
|
|
1307
|
+
composite.addRegistry('providers', providerRegistry);
|
|
1308
|
+
|
|
1309
|
+
const result = composite.can(matrixTestUrn('ext=pdf;op=generate'));
|
|
1310
|
+
assert(result.cap !== null, 'Should return cap');
|
|
1311
|
+
assert(result.compositeHost instanceof CompositeCapSet, 'Should return CompositeCapSet');
|
|
1312
|
+
assert(composite.acceptsRequest(matrixTestUrn('ext=pdf;op=generate')), 'Should accept matching cap');
|
|
1313
|
+
assert(!composite.acceptsRequest(matrixTestUrn('op=nonexistent')), 'Should not accept non-matching cap');
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// TEST123: CapBlock registry management add, get, remove operations
|
|
1317
|
+
function test123_capBlockRegistryManagement() {
|
|
1318
|
+
const composite = new CapBlock();
|
|
1319
|
+
const registry1 = new CapMatrix();
|
|
1320
|
+
const registry2 = new CapMatrix();
|
|
1321
|
+
|
|
1322
|
+
composite.addRegistry('r1', registry1);
|
|
1323
|
+
composite.addRegistry('r2', registry2);
|
|
1324
|
+
assertEqual(composite.getRegistryNames().length, 2, 'Should have 2 registries');
|
|
1325
|
+
|
|
1326
|
+
assertEqual(composite.getRegistry('r1'), registry1, 'Should get correct registry');
|
|
1327
|
+
|
|
1328
|
+
const removed = composite.removeRegistry('r1');
|
|
1329
|
+
assertEqual(removed, registry1, 'Should return removed registry');
|
|
1330
|
+
assertEqual(composite.getRegistryNames().length, 1, 'Should have 1 registry after removal');
|
|
1331
|
+
|
|
1332
|
+
assertEqual(composite.getRegistry('nonexistent'), null, 'Should return null for non-existent');
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// TEST124: CapGraph basic construction builds nodes and edges from caps
|
|
1336
|
+
function test124_capGraphBasicConstruction() {
|
|
1337
|
+
const registry = new CapMatrix();
|
|
1338
|
+
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1339
|
+
|
|
1340
|
+
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1341
|
+
const cap2 = makeGraphCap('media:string', 'media:object', 'String to Object');
|
|
1342
|
+
registry.registerCapSet('converter', mockHost, [cap1, cap2]);
|
|
1343
|
+
|
|
1344
|
+
const cube = new CapBlock();
|
|
1345
|
+
cube.addRegistry('converters', registry);
|
|
1346
|
+
|
|
1347
|
+
const graph = cube.graph();
|
|
1348
|
+
assertEqual(graph.getNodes().size, 3, 'Expected 3 nodes');
|
|
1349
|
+
assertEqual(graph.getEdges().length, 2, 'Expected 2 edges');
|
|
1350
|
+
assertEqual(graph.stats().nodeCount, 3, 'Expected 3 nodes in stats');
|
|
1351
|
+
assertEqual(graph.stats().edgeCount, 2, 'Expected 2 edges in stats');
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// TEST125: CapGraph getOutgoing and getIncoming return correct edges for media URN
|
|
1355
|
+
function test125_capGraphOutgoingIncoming() {
|
|
1356
|
+
const registry = new CapMatrix();
|
|
1357
|
+
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1358
|
+
|
|
1359
|
+
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1360
|
+
const cap2 = makeGraphCap('media:binary', 'media:object', 'Binary to Object');
|
|
1361
|
+
registry.registerCapSet('converter', mockHost, [cap1, cap2]);
|
|
1362
|
+
|
|
1363
|
+
const cube = new CapBlock();
|
|
1364
|
+
cube.addRegistry('converters', registry);
|
|
1365
|
+
const graph = cube.graph();
|
|
1366
|
+
|
|
1367
|
+
assertEqual(graph.getOutgoing('media:binary').length, 2, 'binary should have 2 outgoing');
|
|
1368
|
+
assertEqual(graph.getIncoming('media:string').length, 1, 'string should have 1 incoming');
|
|
1369
|
+
assertEqual(graph.getIncoming('media:object').length, 1, 'object should have 1 incoming');
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// TEST126: CapGraph canConvert checks direct and transitive conversion paths
|
|
1373
|
+
function test126_capGraphCanConvert() {
|
|
1374
|
+
const registry = new CapMatrix();
|
|
1375
|
+
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1376
|
+
|
|
1377
|
+
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1378
|
+
const cap2 = makeGraphCap('media:string', 'media:object', 'String to Object');
|
|
1379
|
+
registry.registerCapSet('converter', mockHost, [cap1, cap2]);
|
|
1380
|
+
|
|
1381
|
+
const cube = new CapBlock();
|
|
1382
|
+
cube.addRegistry('converters', registry);
|
|
1383
|
+
const graph = cube.graph();
|
|
1384
|
+
|
|
1385
|
+
assert(graph.canConvert('media:binary', 'media:string'), 'Direct conversion');
|
|
1386
|
+
assert(graph.canConvert('media:string', 'media:object'), 'Direct conversion');
|
|
1387
|
+
assert(graph.canConvert('media:binary', 'media:object'), 'Transitive conversion');
|
|
1388
|
+
assert(graph.canConvert('media:binary', 'media:binary'), 'Same spec');
|
|
1389
|
+
assert(!graph.canConvert('media:object', 'media:binary'), 'Impossible conversion');
|
|
1390
|
+
assert(!graph.canConvert('media:nonexistent', 'media:string'), 'Nonexistent node');
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// TEST127: CapGraph findPath returns shortest path between media URNs
|
|
1394
|
+
function test127_capGraphFindPath() {
|
|
1395
|
+
const registry = new CapMatrix();
|
|
1396
|
+
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1397
|
+
|
|
1398
|
+
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1399
|
+
const cap2 = makeGraphCap('media:string', 'media:object', 'String to Object');
|
|
1400
|
+
registry.registerCapSet('converter', mockHost, [cap1, cap2]);
|
|
1401
|
+
|
|
1402
|
+
const cube = new CapBlock();
|
|
1403
|
+
cube.addRegistry('converters', registry);
|
|
1404
|
+
const graph = cube.graph();
|
|
1405
|
+
|
|
1406
|
+
// Direct path
|
|
1407
|
+
let path = graph.findPath('media:binary', 'media:string');
|
|
1408
|
+
assert(path !== null, 'Should find direct path');
|
|
1409
|
+
assertEqual(path.length, 1, 'Direct path length should be 1');
|
|
1410
|
+
|
|
1411
|
+
// Transitive path
|
|
1412
|
+
path = graph.findPath('media:binary', 'media:object');
|
|
1413
|
+
assert(path !== null, 'Should find transitive path');
|
|
1414
|
+
assertEqual(path.length, 2, 'Transitive path length should be 2');
|
|
1415
|
+
|
|
1416
|
+
// No path
|
|
1417
|
+
path = graph.findPath('media:object', 'media:binary');
|
|
1418
|
+
assertEqual(path, null, 'Should not find impossible path');
|
|
1419
|
+
|
|
1420
|
+
// Same spec
|
|
1421
|
+
path = graph.findPath('media:binary', 'media:binary');
|
|
1422
|
+
assert(path !== null, 'Same spec should return empty path');
|
|
1423
|
+
assertEqual(path.length, 0, 'Same spec path should be empty');
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// TEST128: CapGraph findAllPaths returns all paths sorted by length
|
|
1427
|
+
function test128_capGraphFindAllPaths() {
|
|
1428
|
+
const registry = new CapMatrix();
|
|
1429
|
+
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1430
|
+
|
|
1431
|
+
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1432
|
+
const cap2 = makeGraphCap('media:string', 'media:object', 'String to Object');
|
|
1433
|
+
const cap3 = makeGraphCap('media:binary', 'media:object', 'Binary to Object (direct)');
|
|
1434
|
+
registry.registerCapSet('converter', mockHost, [cap1, cap2, cap3]);
|
|
1435
|
+
|
|
1436
|
+
const cube = new CapBlock();
|
|
1437
|
+
cube.addRegistry('converters', registry);
|
|
1438
|
+
const graph = cube.graph();
|
|
1439
|
+
|
|
1440
|
+
const paths = graph.findAllPaths('media:binary', 'media:object', 3);
|
|
1441
|
+
assertEqual(paths.length, 2, 'Should find 2 paths');
|
|
1442
|
+
assertEqual(paths[0].length, 1, 'Shortest path first (direct)');
|
|
1443
|
+
assertEqual(paths[1].length, 2, 'Longer path second (via string)');
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// TEST129: CapGraph getDirectEdges returns edges sorted by specificity
|
|
1447
|
+
function test129_capGraphGetDirectEdges() {
|
|
1448
|
+
const registry1 = new CapMatrix();
|
|
1449
|
+
const registry2 = new CapMatrix();
|
|
1450
|
+
const mockHost1 = { executeCap: async () => ({ textOutput: 'mock1' }) };
|
|
1451
|
+
const mockHost2 = { executeCap: async () => ({ textOutput: 'mock2' }) };
|
|
1452
|
+
|
|
1453
|
+
const cap1 = makeGraphCap('media:binary', 'media:string', 'Generic Binary to String');
|
|
1454
|
+
const capUrn2 = CapUrn.fromString('cap:ext=pdf;in="media:binary";op=convert;out="media:string"');
|
|
1455
|
+
const cap2 = new Cap(capUrn2, 'PDF Binary to String', 'convert', 'PDF Binary to String');
|
|
1456
|
+
|
|
1457
|
+
registry1.registerCapSet('converter1', mockHost1, [cap1]);
|
|
1458
|
+
registry2.registerCapSet('converter2', mockHost2, [cap2]);
|
|
1459
|
+
|
|
1460
|
+
const cube = new CapBlock();
|
|
1461
|
+
cube.addRegistry('reg1', registry1);
|
|
1462
|
+
cube.addRegistry('reg2', registry2);
|
|
1463
|
+
const graph = cube.graph();
|
|
1464
|
+
|
|
1465
|
+
const edges = graph.getDirectEdges('media:binary', 'media:string');
|
|
1466
|
+
assertEqual(edges.length, 2, 'Expected 2 direct edges');
|
|
1467
|
+
assertEqual(edges[0].cap.title, 'PDF Binary to String', 'More specific edge first');
|
|
1468
|
+
assert(edges[0].specificity > edges[1].specificity, 'First edge should have higher specificity');
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// TEST130: CapGraph stats returns node count, edge count, input/output URN counts
|
|
1472
|
+
function test130_capGraphStats() {
|
|
1473
|
+
const registry = new CapMatrix();
|
|
1474
|
+
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1475
|
+
|
|
1476
|
+
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1477
|
+
const cap2 = makeGraphCap('media:string', 'media:object', 'String to Object');
|
|
1478
|
+
const cap3 = makeGraphCap('media:binary', 'media:json', 'Binary to JSON');
|
|
1479
|
+
registry.registerCapSet('converter', mockHost, [cap1, cap2, cap3]);
|
|
1480
|
+
|
|
1481
|
+
const cube = new CapBlock();
|
|
1482
|
+
cube.addRegistry('converters', registry);
|
|
1483
|
+
const graph = cube.graph();
|
|
1484
|
+
const stats = graph.stats();
|
|
1485
|
+
|
|
1486
|
+
assertEqual(stats.nodeCount, 4, '4 unique nodes');
|
|
1487
|
+
assertEqual(stats.edgeCount, 3, '3 edges');
|
|
1488
|
+
assertEqual(stats.inputUrnCount, 2, '2 input URNs');
|
|
1489
|
+
assertEqual(stats.outputUrnCount, 3, '3 output URNs');
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// TEST131: CapGraph with CapBlock builds graph from multiple registries
|
|
1493
|
+
function test131_capGraphWithCapBlock() {
|
|
1494
|
+
const providerRegistry = new CapMatrix();
|
|
1495
|
+
const pluginRegistry = new CapMatrix();
|
|
1496
|
+
const providerHost = { executeCap: async () => ({ textOutput: 'provider' }) };
|
|
1497
|
+
const pluginHost = { executeCap: async () => ({ textOutput: 'plugin' }) };
|
|
1498
|
+
|
|
1499
|
+
const providerCap = makeGraphCap('media:binary', 'media:string', 'Provider Binary to String');
|
|
1500
|
+
providerRegistry.registerCapSet('provider', providerHost, [providerCap]);
|
|
1501
|
+
|
|
1502
|
+
const pluginCap = makeGraphCap('media:string', 'media:object', 'Plugin String to Object');
|
|
1503
|
+
pluginRegistry.registerCapSet('plugin', pluginHost, [pluginCap]);
|
|
1504
|
+
|
|
1505
|
+
const cube = new CapBlock();
|
|
1506
|
+
cube.addRegistry('providers', providerRegistry);
|
|
1507
|
+
cube.addRegistry('plugins', pluginRegistry);
|
|
1508
|
+
const graph = cube.graph();
|
|
1509
|
+
|
|
1510
|
+
assert(graph.canConvert('media:binary', 'media:object'), 'Should convert across registries');
|
|
1511
|
+
const path = graph.findPath('media:binary', 'media:object');
|
|
1512
|
+
assert(path !== null, 'Should find path');
|
|
1513
|
+
assertEqual(path.length, 2, 'Path through 2 registries');
|
|
1514
|
+
assertEqual(path[0].registryName, 'providers', 'First edge from providers');
|
|
1515
|
+
assertEqual(path[1].registryName, 'plugins', 'Second edge from plugins');
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// TEST132: N/A (already covered by TEST129)
|
|
1519
|
+
// TEST133: N/A (already covered by TEST131)
|
|
1520
|
+
// TEST134: N/A (already covered by TEST130)
|
|
1521
|
+
|
|
1522
|
+
// ============================================================================
|
|
1523
|
+
// caller.rs: TEST156-TEST159
|
|
1524
|
+
// ============================================================================
|
|
1525
|
+
|
|
1526
|
+
// TEST156: Creating StdinSource Data variant with byte vector
|
|
1527
|
+
function test156_stdinSourceFromData() {
|
|
1528
|
+
const testData = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"
|
|
1529
|
+
const source = StdinSource.fromData(testData);
|
|
1530
|
+
assert(source !== null, 'Should create source');
|
|
1531
|
+
assertEqual(source.kind, StdinSourceKind.DATA, 'Should be DATA kind');
|
|
1532
|
+
assert(source.isData(), 'isData() should return true');
|
|
1533
|
+
assert(!source.isFileReference(), 'isFileReference() should return false');
|
|
1534
|
+
assertEqual(source.data, testData, 'Should store data');
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// TEST157: Creating StdinSource FileReference variant with all required fields
|
|
1538
|
+
function test157_stdinSourceFromFileReference() {
|
|
1539
|
+
const trackedFileId = 'tracked-file-123';
|
|
1540
|
+
const originalPath = '/path/to/original.pdf';
|
|
1541
|
+
const securityBookmark = new Uint8Array([0x62, 0x6f, 0x6f, 0x6b]);
|
|
1542
|
+
const mediaUrn = 'media:pdf';
|
|
1543
|
+
|
|
1544
|
+
const source = StdinSource.fromFileReference(trackedFileId, originalPath, securityBookmark, mediaUrn);
|
|
1545
|
+
assert(source !== null, 'Should create source');
|
|
1546
|
+
assertEqual(source.kind, StdinSourceKind.FILE_REFERENCE, 'Should be FILE_REFERENCE kind');
|
|
1547
|
+
assert(!source.isData(), 'isData() should return false');
|
|
1548
|
+
assert(source.isFileReference(), 'isFileReference() should return true');
|
|
1549
|
+
assertEqual(source.trackedFileId, trackedFileId, 'Should store trackedFileId');
|
|
1550
|
+
assertEqual(source.originalPath, originalPath, 'Should store originalPath');
|
|
1551
|
+
assertEqual(source.mediaUrn, mediaUrn, 'Should store mediaUrn');
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// TEST158: StdinSource Data with empty vector stores and retrieves correctly
|
|
1555
|
+
function test158_stdinSourceWithEmptyData() {
|
|
1556
|
+
const emptyData = new Uint8Array(0);
|
|
1557
|
+
const source = StdinSource.fromData(emptyData);
|
|
1558
|
+
assert(source.isData(), 'Should be data source');
|
|
1559
|
+
assertEqual(source.data.length, 0, 'Data length should be 0');
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// TEST159: StdinSource Data with binary content like PNG header bytes
|
|
1563
|
+
function test159_stdinSourceWithBinaryContent() {
|
|
1564
|
+
const pngHeader = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
|
1565
|
+
const source = StdinSource.fromData(pngHeader);
|
|
1566
|
+
assert(source.isData(), 'Should be data source');
|
|
1567
|
+
assertEqual(source.data.length, 8, 'Should have 8 bytes');
|
|
1568
|
+
assertEqual(source.data[0], 0x89, 'First byte should be 0x89');
|
|
1569
|
+
assertEqual(source.data[1], 0x50, 'Second byte should be 0x50 (P)');
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// ============================================================================
|
|
1573
|
+
// caller.rs: TEST274-TEST283
|
|
1574
|
+
// ============================================================================
|
|
1575
|
+
|
|
1576
|
+
// TEST274: CapArgumentValue constructor stores media_urn and raw byte value
|
|
1577
|
+
function test274_capArgumentValueNew() {
|
|
1578
|
+
const arg = new CapArgumentValue('media:model-spec;textable', new Uint8Array([103, 112, 116, 45, 52]));
|
|
1579
|
+
assertEqual(arg.mediaUrn, 'media:model-spec;textable', 'mediaUrn must match');
|
|
1580
|
+
assertEqual(arg.value.length, 5, 'value must have 5 bytes');
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// TEST275: CapArgumentValue.fromStr converts string to UTF-8 bytes
|
|
1584
|
+
function test275_capArgumentValueFromStr() {
|
|
1585
|
+
const arg = CapArgumentValue.fromStr('media:string;textable', 'hello world');
|
|
1586
|
+
assertEqual(arg.mediaUrn, 'media:string;textable', 'mediaUrn must match');
|
|
1587
|
+
assertEqual(new TextDecoder().decode(arg.value), 'hello world', 'value must decode correctly');
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// TEST276: CapArgumentValue.valueAsStr succeeds for UTF-8 data
|
|
1591
|
+
function test276_capArgumentValueAsStrValid() {
|
|
1592
|
+
const arg = CapArgumentValue.fromStr('media:string', 'test');
|
|
1593
|
+
assertEqual(arg.valueAsStr(), 'test', 'valueAsStr must return test');
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// TEST277: CapArgumentValue.valueAsStr fails for non-UTF-8 binary data
|
|
1597
|
+
function test277_capArgumentValueAsStrInvalidUtf8() {
|
|
1598
|
+
const arg = new CapArgumentValue('media:pdf', new Uint8Array([0xFF, 0xFE, 0x80]));
|
|
1599
|
+
let threw = false;
|
|
1600
|
+
try {
|
|
1601
|
+
arg.valueAsStr();
|
|
1602
|
+
} catch (e) {
|
|
1603
|
+
threw = true;
|
|
1604
|
+
}
|
|
1605
|
+
assert(threw, 'non-UTF-8 data must fail on valueAsStr with fatal decoder');
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// TEST278: CapArgumentValue with empty value stores empty Uint8Array
|
|
1609
|
+
function test278_capArgumentValueEmpty() {
|
|
1610
|
+
const arg = new CapArgumentValue('media:void', new Uint8Array([]));
|
|
1611
|
+
assertEqual(arg.value.length, 0, 'empty value must have 0 bytes');
|
|
1612
|
+
assertEqual(arg.valueAsStr(), '', 'empty value as string must be empty string');
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// TEST279-281: N/A for JS (Rust Debug/Clone/Send traits)
|
|
1616
|
+
|
|
1617
|
+
// TEST282: CapArgumentValue.fromStr with Unicode string preserves all characters
|
|
1618
|
+
function test282_capArgumentValueUnicode() {
|
|
1619
|
+
const arg = CapArgumentValue.fromStr('media:string', 'hello \u4e16\u754c \ud83c\udf0d');
|
|
1620
|
+
assertEqual(arg.valueAsStr(), 'hello \u4e16\u754c \ud83c\udf0d', 'Unicode must roundtrip');
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// TEST283: CapArgumentValue with large binary payload preserves all bytes
|
|
1624
|
+
function test283_capArgumentValueLargeBinary() {
|
|
1625
|
+
const data = new Uint8Array(10000);
|
|
1626
|
+
for (let i = 0; i < 10000; i++) {
|
|
1627
|
+
data[i] = i % 256;
|
|
1628
|
+
}
|
|
1629
|
+
const arg = new CapArgumentValue('media:pdf', data);
|
|
1630
|
+
assertEqual(arg.value.length, 10000, 'large binary must preserve all bytes');
|
|
1631
|
+
assertEqual(arg.value[0], 0, 'first byte check');
|
|
1632
|
+
assertEqual(arg.value[255], 255, 'byte 255 check');
|
|
1633
|
+
assertEqual(arg.value[256], 0, 'byte 256 wraps check');
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// ============================================================================
|
|
1637
|
+
// standard/caps.rs: TEST304-TEST312
|
|
1638
|
+
// ============================================================================
|
|
1639
|
+
|
|
1640
|
+
const { TaggedUrn } = require('tagged-urn');
|
|
1641
|
+
|
|
1642
|
+
// TEST304: MEDIA_AVAILABILITY_OUTPUT constant parses as valid media URN with correct tags
|
|
1643
|
+
function test304_mediaAvailabilityOutputConstant() {
|
|
1644
|
+
const urn = TaggedUrn.fromString(MEDIA_AVAILABILITY_OUTPUT);
|
|
1645
|
+
assert(urn.getTag('textable') !== undefined, 'model-availability must be textable');
|
|
1646
|
+
assertEqual(urn.getTag('record'), '*', 'model-availability must be record');
|
|
1647
|
+
assert(urn.getTag('textable') !== undefined, 'model-availability must not be binary (has textable)');
|
|
1648
|
+
const reparsed = TaggedUrn.fromString(urn.toString());
|
|
1649
|
+
assert(urn.conformsTo(reparsed), 'roundtrip must match original');
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// TEST305: MEDIA_PATH_OUTPUT constant parses as valid media URN with correct tags
|
|
1653
|
+
function test305_mediaPathOutputConstant() {
|
|
1654
|
+
const urn = TaggedUrn.fromString(MEDIA_PATH_OUTPUT);
|
|
1655
|
+
assert(urn.getTag('textable') !== undefined, 'model-path must be textable');
|
|
1656
|
+
assertEqual(urn.getTag('record'), '*', 'model-path must be record');
|
|
1657
|
+
assert(urn.getTag('textable') !== undefined, 'model-path must not be binary (has textable)');
|
|
1658
|
+
const reparsed = TaggedUrn.fromString(urn.toString());
|
|
1659
|
+
assert(urn.conformsTo(reparsed), 'roundtrip must match original');
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// TEST306: MEDIA_AVAILABILITY_OUTPUT and MEDIA_PATH_OUTPUT are distinct URNs
|
|
1663
|
+
function test306_availabilityAndPathOutputDistinct() {
|
|
1664
|
+
assert(MEDIA_AVAILABILITY_OUTPUT !== MEDIA_PATH_OUTPUT, 'Must be distinct');
|
|
1665
|
+
const avail = TaggedUrn.fromString(MEDIA_AVAILABILITY_OUTPUT);
|
|
1666
|
+
const path = TaggedUrn.fromString(MEDIA_PATH_OUTPUT);
|
|
1667
|
+
let matchResult;
|
|
1668
|
+
try {
|
|
1669
|
+
matchResult = avail.conformsTo(path);
|
|
1670
|
+
} catch (e) {
|
|
1671
|
+
matchResult = false;
|
|
1672
|
+
}
|
|
1673
|
+
assert(!matchResult, 'availability must not conform to path');
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// TEST307: model_availability_urn builds valid cap URN with correct op and media specs
|
|
1677
|
+
function test307_modelAvailabilityUrn() {
|
|
1678
|
+
const urn = modelAvailabilityUrn();
|
|
1679
|
+
assert(urn.hasTag('op', 'model-availability'), 'Must have op=model-availability');
|
|
1680
|
+
const inSpec = TaggedUrn.fromString(urn.getInSpec());
|
|
1681
|
+
const expectedIn = TaggedUrn.fromString(MEDIA_MODEL_SPEC);
|
|
1682
|
+
assert(inSpec.conformsTo(expectedIn), 'input must conform to MEDIA_MODEL_SPEC');
|
|
1683
|
+
const outSpec = TaggedUrn.fromString(urn.getOutSpec());
|
|
1684
|
+
const expectedOut = TaggedUrn.fromString(MEDIA_AVAILABILITY_OUTPUT);
|
|
1685
|
+
assert(outSpec.conformsTo(expectedOut), 'output must conform to MEDIA_AVAILABILITY_OUTPUT');
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// TEST308: model_path_urn builds valid cap URN with correct op and media specs
|
|
1689
|
+
function test308_modelPathUrn() {
|
|
1690
|
+
const urn = modelPathUrn();
|
|
1691
|
+
assert(urn.hasTag('op', 'model-path'), 'Must have op=model-path');
|
|
1692
|
+
const inSpec = TaggedUrn.fromString(urn.getInSpec());
|
|
1693
|
+
const expectedIn = TaggedUrn.fromString(MEDIA_MODEL_SPEC);
|
|
1694
|
+
assert(inSpec.conformsTo(expectedIn), 'input must conform to MEDIA_MODEL_SPEC');
|
|
1695
|
+
const outSpec = TaggedUrn.fromString(urn.getOutSpec());
|
|
1696
|
+
const expectedOut = TaggedUrn.fromString(MEDIA_PATH_OUTPUT);
|
|
1697
|
+
assert(outSpec.conformsTo(expectedOut), 'output must conform to MEDIA_PATH_OUTPUT');
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// TEST309: model_availability_urn and model_path_urn produce distinct URNs
|
|
1701
|
+
function test309_modelAvailabilityAndPathAreDistinct() {
|
|
1702
|
+
const avail = modelAvailabilityUrn();
|
|
1703
|
+
const path = modelPathUrn();
|
|
1704
|
+
assert(avail.toString() !== path.toString(), 'availability and path must be distinct');
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// TEST310: llm_conversation_urn uses unconstrained tag (not constrained)
|
|
1708
|
+
function test310_llmConversationUrnUnconstrained() {
|
|
1709
|
+
const urn = llmConversationUrn('en');
|
|
1710
|
+
assert(urn.getTag('unconstrained') !== undefined, 'Must have unconstrained tag');
|
|
1711
|
+
assert(urn.hasTag('op', 'conversation'), 'Must have op=conversation');
|
|
1712
|
+
assert(urn.hasTag('language', 'en'), 'Must have language=en');
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// TEST311: llm_conversation_urn in/out specs match the expected media URNs semantically
|
|
1716
|
+
function test311_llmConversationUrnSpecs() {
|
|
1717
|
+
const urn = llmConversationUrn('fr');
|
|
1718
|
+
const inSpec = TaggedUrn.fromString(urn.getInSpec());
|
|
1719
|
+
const expectedIn = TaggedUrn.fromString(MEDIA_STRING);
|
|
1720
|
+
assert(inSpec.conformsTo(expectedIn), 'in_spec must conform to MEDIA_STRING');
|
|
1721
|
+
const outSpec = TaggedUrn.fromString(urn.getOutSpec());
|
|
1722
|
+
const expectedOut = TaggedUrn.fromString(MEDIA_LLM_INFERENCE_OUTPUT);
|
|
1723
|
+
assert(outSpec.conformsTo(expectedOut), 'out_spec must conform to MEDIA_LLM_INFERENCE_OUTPUT');
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// TEST312: All URN builders produce parseable cap URNs
|
|
1727
|
+
function test312_allUrnBuildersProduceValidUrns() {
|
|
1728
|
+
const avail = modelAvailabilityUrn();
|
|
1729
|
+
const path = modelPathUrn();
|
|
1730
|
+
const conv = llmConversationUrn('en');
|
|
1731
|
+
|
|
1732
|
+
const parsedAvail = CapUrn.fromString(avail.toString());
|
|
1733
|
+
assert(parsedAvail !== null, 'modelAvailabilityUrn must be parseable');
|
|
1734
|
+
const parsedPath = CapUrn.fromString(path.toString());
|
|
1735
|
+
assert(parsedPath !== null, 'modelPathUrn must be parseable');
|
|
1736
|
+
const parsedConv = CapUrn.fromString(conv.toString());
|
|
1737
|
+
assert(parsedConv !== null, 'llmConversationUrn must be parseable');
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// ============================================================================
|
|
1741
|
+
// Additional JS-specific tests (extension index, media URN resolution, Cap JSON)
|
|
1742
|
+
// ============================================================================
|
|
1743
|
+
|
|
1744
|
+
// These tests cover JS-specific functionality not in the Rust numbering scheme
|
|
1745
|
+
// but are important for capdag-js correctness.
|
|
1746
|
+
|
|
1747
|
+
function testJS_buildExtensionIndex() {
|
|
1748
|
+
const mediaSpecs = [
|
|
1749
|
+
{ urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
|
|
1750
|
+
{ urn: 'media:image;jpeg', media_type: 'image/jpeg', extensions: ['jpg', 'jpeg'] },
|
|
1751
|
+
{ urn: 'media:json;textable', media_type: 'application/json', extensions: ['json'] }
|
|
1752
|
+
];
|
|
1753
|
+
const index = buildExtensionIndex(mediaSpecs);
|
|
1754
|
+
assert(index instanceof Map, 'Should return a Map');
|
|
1755
|
+
assertEqual(index.size, 4, 'Should have 4 extensions');
|
|
1756
|
+
assert(index.has('pdf'), 'Should have pdf');
|
|
1757
|
+
assert(index.has('jpg'), 'Should have jpg');
|
|
1758
|
+
assert(index.has('jpeg'), 'Should have jpeg');
|
|
1759
|
+
assert(index.has('json'), 'Should have json');
|
|
1760
|
+
assertEqual(index.get('pdf')[0], 'media:pdf', 'pdf should map correctly');
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
function testJS_mediaUrnsForExtension() {
|
|
1764
|
+
const mediaSpecs = [
|
|
1765
|
+
{ urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
|
|
1766
|
+
{ urn: 'media:json;textable;record', media_type: 'application/json', extensions: ['json'] },
|
|
1767
|
+
{ urn: 'media:json;textable;list', media_type: 'application/json', extensions: ['json'] }
|
|
1768
|
+
];
|
|
1769
|
+
|
|
1770
|
+
const pdfUrns = mediaUrnsForExtension('pdf', mediaSpecs);
|
|
1771
|
+
assertEqual(pdfUrns.length, 1, 'Should find 1 URN for pdf');
|
|
1772
|
+
|
|
1773
|
+
// Case insensitivity
|
|
1774
|
+
const pdfUrnsUpper = mediaUrnsForExtension('PDF', mediaSpecs);
|
|
1775
|
+
assertEqual(pdfUrnsUpper.length, 1, 'Should find URN with uppercase extension');
|
|
1776
|
+
|
|
1777
|
+
// Multiple URNs for same extension
|
|
1778
|
+
const jsonUrns = mediaUrnsForExtension('json', mediaSpecs);
|
|
1779
|
+
assertEqual(jsonUrns.length, 2, 'Should find 2 URNs for json');
|
|
1780
|
+
|
|
1781
|
+
// Unknown extension throws
|
|
1782
|
+
let thrownError = null;
|
|
1783
|
+
try {
|
|
1784
|
+
mediaUrnsForExtension('unknown', mediaSpecs);
|
|
1785
|
+
} catch (e) {
|
|
1786
|
+
thrownError = e;
|
|
1787
|
+
}
|
|
1788
|
+
assert(thrownError instanceof MediaSpecError, 'Should throw MediaSpecError for unknown ext');
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function testJS_getExtensionMappings() {
|
|
1792
|
+
const mediaSpecs = [
|
|
1793
|
+
{ urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
|
|
1794
|
+
{ urn: 'media:image;jpeg', media_type: 'image/jpeg', extensions: ['jpg', 'jpeg'] }
|
|
1795
|
+
];
|
|
1796
|
+
const mappings = getExtensionMappings(mediaSpecs);
|
|
1797
|
+
assert(Array.isArray(mappings), 'Should return an array');
|
|
1798
|
+
assertEqual(mappings.length, 3, 'Should have 3 mappings');
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function testJS_capWithMediaSpecs() {
|
|
1802
|
+
const urn = CapUrn.fromString('cap:in="media:string";op=test;out="media:custom"');
|
|
1803
|
+
const cap = new Cap(urn, 'Test Cap', 'test_command');
|
|
1804
|
+
cap.mediaSpecs = [
|
|
1805
|
+
{ urn: MEDIA_STRING, media_type: 'text/plain', title: 'String', profile_uri: 'https://capdag.com/schema/str' },
|
|
1806
|
+
{ urn: 'media:custom', media_type: 'application/json', title: 'Custom Output', schema: { type: 'object' } }
|
|
1807
|
+
];
|
|
1808
|
+
const strSpec = cap.resolveMediaUrn(MEDIA_STRING);
|
|
1809
|
+
assertEqual(strSpec.contentType, 'text/plain', 'Should resolve string spec');
|
|
1810
|
+
const outputSpec = cap.resolveMediaUrn('media:custom');
|
|
1811
|
+
assertEqual(outputSpec.contentType, 'application/json', 'Should resolve custom spec');
|
|
1812
|
+
assert(outputSpec.schema !== null, 'Should have schema');
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function testJS_capJSONSerialization() {
|
|
1816
|
+
const urn = CapUrn.fromString(testUrn('op=test'));
|
|
1817
|
+
const cap = new Cap(urn, 'Test Cap', 'test_command');
|
|
1818
|
+
cap.mediaSpecs = [
|
|
1819
|
+
{ urn: 'media:custom', media_type: 'text/plain', title: 'Custom' }
|
|
1820
|
+
];
|
|
1821
|
+
cap.arguments = {
|
|
1822
|
+
required: [{ name: 'input', media_urn: MEDIA_STRING }],
|
|
1823
|
+
optional: []
|
|
1824
|
+
};
|
|
1825
|
+
cap.output = { media_urn: 'media:custom', output_description: 'Test output' };
|
|
1826
|
+
|
|
1827
|
+
const json = cap.toJSON();
|
|
1828
|
+
assert(json.media_specs !== undefined, 'Should have media_specs');
|
|
1829
|
+
assertEqual(typeof json.urn, 'string', 'URN should be string');
|
|
1830
|
+
|
|
1831
|
+
const restored = Cap.fromJSON(json);
|
|
1832
|
+
assert(restored.mediaSpecs !== undefined, 'Should restore mediaSpecs');
|
|
1833
|
+
assertEqual(restored.urn.getInSpec(), MEDIA_VOID, 'Should restore inSpec');
|
|
1834
|
+
assertEqual(restored.urn.getOutSpec(), MEDIA_OBJECT, 'Should restore outSpec');
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function testJS_stdinSourceKindConstants() {
|
|
1838
|
+
assert(StdinSourceKind.DATA !== undefined, 'DATA kind should be defined');
|
|
1839
|
+
assert(StdinSourceKind.FILE_REFERENCE !== undefined, 'FILE_REFERENCE kind should be defined');
|
|
1840
|
+
assert(StdinSourceKind.DATA !== StdinSourceKind.FILE_REFERENCE, 'Kind values should be distinct');
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function testJS_stdinSourceNullData() {
|
|
1844
|
+
const source = StdinSource.fromData(null);
|
|
1845
|
+
assert(source !== null, 'Should create source');
|
|
1846
|
+
assert(source.isData(), 'Should be data source');
|
|
1847
|
+
assertEqual(source.data, null, 'Data should be null');
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function testJS_argsPassedToExecuteCap() {
|
|
1851
|
+
let receivedArgs = null;
|
|
1852
|
+
const mockHost = {
|
|
1853
|
+
executeCap: async (capUrn, args) => {
|
|
1854
|
+
receivedArgs = args;
|
|
1855
|
+
return { textOutput: 'ok' };
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
const cap = new Cap(
|
|
1860
|
+
CapUrn.fromString('cap:in="media:void";op=test;out="media:string"'),
|
|
1861
|
+
'Test Cap',
|
|
1862
|
+
'test-command'
|
|
1863
|
+
);
|
|
1864
|
+
const registry = new CapMatrix();
|
|
1865
|
+
registry.registerCapSet('test', mockHost, [cap]);
|
|
1866
|
+
const cube = new CapBlock();
|
|
1867
|
+
cube.addRegistry('test', registry);
|
|
1868
|
+
|
|
1869
|
+
const args = [new CapArgumentValue('media:void', new Uint8Array([1, 2, 3]))];
|
|
1870
|
+
const { compositeHost } = cube.can('cap:in="media:void";op=test;out="media:string"');
|
|
1871
|
+
|
|
1872
|
+
return compositeHost.executeCap(
|
|
1873
|
+
'cap:in="media:void";op=test;out="media:string"',
|
|
1874
|
+
args
|
|
1875
|
+
).then(() => {
|
|
1876
|
+
assert(receivedArgs !== null, 'Should receive arguments');
|
|
1877
|
+
assert(Array.isArray(receivedArgs), 'Should receive array');
|
|
1878
|
+
assertEqual(receivedArgs.length, 1, 'Should have one argument');
|
|
1879
|
+
assertEqual(receivedArgs[0].mediaUrn, 'media:void', 'Correct mediaUrn');
|
|
1880
|
+
assertEqual(receivedArgs[0].value.length, 3, 'Correct data length');
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function testJS_binaryArgPassedToExecuteCap() {
|
|
1885
|
+
let receivedArgs = null;
|
|
1886
|
+
const mockHost = {
|
|
1887
|
+
executeCap: async (capUrn, args) => {
|
|
1888
|
+
receivedArgs = args;
|
|
1889
|
+
return { textOutput: 'ok' };
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
|
|
1893
|
+
const cap = new Cap(
|
|
1894
|
+
CapUrn.fromString('cap:in="media:void";op=test;out="media:string"'),
|
|
1895
|
+
'Test Cap',
|
|
1896
|
+
'test-command'
|
|
1897
|
+
);
|
|
1898
|
+
const registry = new CapMatrix();
|
|
1899
|
+
registry.registerCapSet('test', mockHost, [cap]);
|
|
1900
|
+
const cube = new CapBlock();
|
|
1901
|
+
cube.addRegistry('test', registry);
|
|
1902
|
+
|
|
1903
|
+
const binaryArg = new CapArgumentValue('media:pdf', new Uint8Array([0x89, 0x50, 0x4E, 0x47]));
|
|
1904
|
+
const { compositeHost } = cube.can('cap:in="media:void";op=test;out="media:string"');
|
|
1905
|
+
|
|
1906
|
+
return compositeHost.executeCap(
|
|
1907
|
+
'cap:in="media:void";op=test;out="media:string"',
|
|
1908
|
+
[binaryArg]
|
|
1909
|
+
).then(() => {
|
|
1910
|
+
assert(receivedArgs !== null, 'Should receive arguments');
|
|
1911
|
+
assertEqual(receivedArgs[0].mediaUrn, 'media:pdf', 'Correct mediaUrn');
|
|
1912
|
+
assertEqual(receivedArgs[0].value[0], 0x89, 'First byte check');
|
|
1913
|
+
assertEqual(receivedArgs[0].value.length, 4, 'Correct data length');
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function testJS_mediaSpecConstruction() {
|
|
1918
|
+
const spec1 = new MediaSpec('text/plain', 'https://capdag.com/schema/str', null, 'String', null, 'media:string');
|
|
1919
|
+
assertEqual(spec1.contentType, 'text/plain', 'Should have content type');
|
|
1920
|
+
assertEqual(spec1.profile, 'https://capdag.com/schema/str', 'Should have profile');
|
|
1921
|
+
assertEqual(spec1.title, 'String', 'Should have title');
|
|
1922
|
+
assertEqual(spec1.mediaUrn, 'media:string', 'Should have mediaUrn');
|
|
1923
|
+
|
|
1924
|
+
const spec2 = new MediaSpec('application/octet-stream', null, null, 'Binary', null, 'media:binary');
|
|
1925
|
+
assertEqual(spec2.profile, null, 'Should have null profile');
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// =============================================================================
|
|
1929
|
+
// Plugin Repository Tests (TEST320-TEST335)
|
|
1930
|
+
// =============================================================================
|
|
1931
|
+
|
|
1932
|
+
// Sample registry for testing
|
|
1933
|
+
const sampleRegistry = {
|
|
1934
|
+
schemaVersion: '3.0',
|
|
1935
|
+
lastUpdated: '2026-02-07T16:48:28Z',
|
|
1936
|
+
plugins: {
|
|
1937
|
+
pdfcartridge: {
|
|
1938
|
+
name: 'pdfcartridge',
|
|
1939
|
+
description: 'PDF document processor',
|
|
1940
|
+
author: 'test-author',
|
|
1941
|
+
pageUrl: 'https://example.com/pdf',
|
|
1942
|
+
teamId: 'P336JK947M',
|
|
1943
|
+
minAppVersion: '1.0.0',
|
|
1944
|
+
categories: ['document'],
|
|
1945
|
+
tags: ['pdf', 'extractor'],
|
|
1946
|
+
caps: [
|
|
1947
|
+
{
|
|
1948
|
+
urn: 'cap:in="media:pdf";op=disbind;out="media:disbound-page;textable;list"',
|
|
1949
|
+
title: 'Disbind PDF',
|
|
1950
|
+
description: 'Extract pages'
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
urn: 'cap:in="media:pdf";op=extract_metadata;out="media:file-metadata;textable;record"',
|
|
1954
|
+
title: 'Extract Metadata',
|
|
1955
|
+
description: 'Get PDF metadata'
|
|
1956
|
+
}
|
|
1957
|
+
],
|
|
1958
|
+
latestVersion: '0.81.5325',
|
|
1959
|
+
versions: {
|
|
1960
|
+
'0.81.5325': {
|
|
1961
|
+
releaseDate: '2026-02-07T16:40:28Z',
|
|
1962
|
+
changelog: ['Initial release'],
|
|
1963
|
+
minAppVersion: '1.0.0',
|
|
1964
|
+
platform: 'darwin-arm64',
|
|
1965
|
+
package: {
|
|
1966
|
+
name: 'pdfcartridge-0.81.5325.pkg',
|
|
1967
|
+
sha256: '9b68724eb9220ecf01e8ed4f5f80c594fbac2239bc5bf675005ec882ecc5eba0',
|
|
1968
|
+
size: 5187485
|
|
1969
|
+
},
|
|
1970
|
+
binary: {
|
|
1971
|
+
name: 'pdfcartridge-0.81.5325-darwin-arm64',
|
|
1972
|
+
sha256: '908187ec35632758f1a00452ff4755ba01020ea288619098b6998d5d33851d19',
|
|
1973
|
+
size: 12980288
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
txtcartridge: {
|
|
1979
|
+
name: 'txtcartridge',
|
|
1980
|
+
description: 'Text file processor',
|
|
1981
|
+
author: 'test-author',
|
|
1982
|
+
pageUrl: 'https://example.com/txt',
|
|
1983
|
+
teamId: 'P336JK947M',
|
|
1984
|
+
minAppVersion: '1.0.0',
|
|
1985
|
+
categories: ['text'],
|
|
1986
|
+
tags: ['txt', 'text'],
|
|
1987
|
+
caps: [
|
|
1988
|
+
{
|
|
1989
|
+
urn: 'cap:in="media:txt;textable";op=disbind;out="media:disbound-page;textable;list"',
|
|
1990
|
+
title: 'Disbind Text',
|
|
1991
|
+
description: 'Extract text pages'
|
|
1992
|
+
}
|
|
1993
|
+
],
|
|
1994
|
+
latestVersion: '0.54.6408',
|
|
1995
|
+
versions: {
|
|
1996
|
+
'0.54.6408': {
|
|
1997
|
+
releaseDate: '2026-02-07T17:44:00Z',
|
|
1998
|
+
changelog: ['First version'],
|
|
1999
|
+
minAppVersion: '1.0.0',
|
|
2000
|
+
platform: 'darwin-arm64',
|
|
2001
|
+
package: {
|
|
2002
|
+
name: 'txtcartridge-0.54.6408.pkg',
|
|
2003
|
+
sha256: 'abc123',
|
|
2004
|
+
size: 821000
|
|
2005
|
+
},
|
|
2006
|
+
binary: {
|
|
2007
|
+
name: 'txtcartridge-0.54.6408-darwin-arm64',
|
|
2008
|
+
sha256: 'def456',
|
|
2009
|
+
size: 1700000
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
|
|
2017
|
+
// TEST320: Plugin info construction
|
|
2018
|
+
function test320_pluginInfoConstruction() {
|
|
2019
|
+
const data = {
|
|
2020
|
+
id: 'testplugin',
|
|
2021
|
+
name: 'Test Plugin',
|
|
2022
|
+
version: '1.0.0',
|
|
2023
|
+
description: 'A test',
|
|
2024
|
+
teamId: 'TEAM123',
|
|
2025
|
+
signedAt: '2026-01-01',
|
|
2026
|
+
binaryName: 'test-binary',
|
|
2027
|
+
binarySha256: 'abc123',
|
|
2028
|
+
caps: [{urn: 'cap:in="media:void";op=test;out="media:void"', title: 'Test', description: ''}]
|
|
2029
|
+
};
|
|
2030
|
+
const plugin = new PluginInfo(data);
|
|
2031
|
+
assert(plugin.id === 'testplugin', 'ID should match');
|
|
2032
|
+
assert(plugin.teamId === 'TEAM123', 'Team ID should match');
|
|
2033
|
+
assert(plugin.caps.length === 1, 'Should have 1 cap');
|
|
2034
|
+
assert(plugin.caps[0].urn === 'cap:in="media:void";op=test;out="media:void"', 'Cap URN should match');
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// TEST321: Plugin info is signed check
|
|
2038
|
+
function test321_pluginInfoIsSigned() {
|
|
2039
|
+
const signed = new PluginInfo({id: 'test', teamId: 'TEAM', signedAt: '2026-01-01', caps: []});
|
|
2040
|
+
assert(signed.isSigned() === true, 'Plugin with teamId and signedAt should be signed');
|
|
2041
|
+
|
|
2042
|
+
const unsigned1 = new PluginInfo({id: 'test', teamId: '', signedAt: '2026-01-01', caps: []});
|
|
2043
|
+
assert(unsigned1.isSigned() === false, 'Plugin without teamId should not be signed');
|
|
2044
|
+
|
|
2045
|
+
const unsigned2 = new PluginInfo({id: 'test', teamId: 'TEAM', signedAt: '', caps: []});
|
|
2046
|
+
assert(unsigned2.isSigned() === false, 'Plugin without signedAt should not be signed');
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// TEST322: Plugin info has binary check
|
|
2050
|
+
function test322_pluginInfoHasBinary() {
|
|
2051
|
+
const withBinary = new PluginInfo({id: 'test', binaryName: 'test-bin', binarySha256: 'abc', caps: []});
|
|
2052
|
+
assert(withBinary.hasBinary() === true, 'Plugin with binary info should return true');
|
|
2053
|
+
|
|
2054
|
+
const noBinary1 = new PluginInfo({id: 'test', binaryName: '', binarySha256: 'abc', caps: []});
|
|
2055
|
+
assert(noBinary1.hasBinary() === false, 'Plugin without binaryName should return false');
|
|
2056
|
+
|
|
2057
|
+
const noBinary2 = new PluginInfo({id: 'test', binaryName: 'test', binarySha256: '', caps: []});
|
|
2058
|
+
assert(noBinary2.hasBinary() === false, 'Plugin without binarySha256 should return false');
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// TEST323: PluginRepoServer validate registry
|
|
2062
|
+
function test323_pluginRepoServerValidateRegistry() {
|
|
2063
|
+
// Valid registry
|
|
2064
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2065
|
+
assert(server.registry.schemaVersion === '3.0', 'Should accept valid registry');
|
|
2066
|
+
|
|
2067
|
+
// Invalid schema version
|
|
2068
|
+
let threw = false;
|
|
2069
|
+
try {
|
|
2070
|
+
new PluginRepoServer({schemaVersion: '2.0', plugins: {}});
|
|
2071
|
+
} catch (e) {
|
|
2072
|
+
threw = true;
|
|
2073
|
+
assert(e.message.includes('schema version'), 'Should reject wrong schema version');
|
|
2074
|
+
}
|
|
2075
|
+
assert(threw, 'Should throw for invalid schema');
|
|
2076
|
+
|
|
2077
|
+
// Missing plugins
|
|
2078
|
+
threw = false;
|
|
2079
|
+
try {
|
|
2080
|
+
new PluginRepoServer({schemaVersion: '3.0'});
|
|
2081
|
+
} catch (e) {
|
|
2082
|
+
threw = true;
|
|
2083
|
+
assert(e.message.includes('plugins'), 'Should reject missing plugins');
|
|
2084
|
+
}
|
|
2085
|
+
assert(threw, 'Should throw for missing plugins');
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// TEST324: PluginRepoServer transform to array
|
|
2089
|
+
function test324_pluginRepoServerTransformToArray() {
|
|
2090
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2091
|
+
const plugins = server.transformToPluginArray();
|
|
2092
|
+
|
|
2093
|
+
assert(Array.isArray(plugins), 'Should return array');
|
|
2094
|
+
assert(plugins.length === 2, 'Should have 2 plugins');
|
|
2095
|
+
|
|
2096
|
+
const pdf = plugins.find(p => p.id === 'pdfcartridge');
|
|
2097
|
+
assert(pdf !== undefined, 'Should include pdfcartridge');
|
|
2098
|
+
assert(pdf.version === '0.81.5325', 'Should have latest version');
|
|
2099
|
+
assert(pdf.teamId === 'P336JK947M', 'Should have teamId');
|
|
2100
|
+
assert(pdf.signedAt === '2026-02-07T16:40:28Z', 'Should have signedAt from releaseDate');
|
|
2101
|
+
assert(pdf.binaryName === 'pdfcartridge-0.81.5325-darwin-arm64', 'Should have binary name');
|
|
2102
|
+
assert(pdf.binarySha256 === '908187ec35632758f1a00452ff4755ba01020ea288619098b6998d5d33851d19', 'Should have SHA256');
|
|
2103
|
+
assert(Array.isArray(pdf.caps), 'Should have caps array');
|
|
2104
|
+
assert(pdf.caps.length === 2, 'Should have 2 caps');
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// TEST325: PluginRepoServer get plugins
|
|
2108
|
+
function test325_pluginRepoServerGetPlugins() {
|
|
2109
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2110
|
+
const response = server.getPlugins();
|
|
2111
|
+
|
|
2112
|
+
assert(response.plugins !== undefined, 'Should have plugins field');
|
|
2113
|
+
assert(Array.isArray(response.plugins), 'Plugins should be array');
|
|
2114
|
+
assert(response.plugins.length === 2, 'Should have 2 plugins');
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// TEST326: PluginRepoServer get plugin by ID
|
|
2118
|
+
function test326_pluginRepoServerGetPluginById() {
|
|
2119
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2120
|
+
|
|
2121
|
+
const pdf = server.getPluginById('pdfcartridge');
|
|
2122
|
+
assert(pdf !== undefined, 'Should find pdfcartridge');
|
|
2123
|
+
assert(pdf.id === 'pdfcartridge', 'Should have correct ID');
|
|
2124
|
+
|
|
2125
|
+
const notFound = server.getPluginById('nonexistent');
|
|
2126
|
+
assert(notFound === undefined, 'Should return undefined for missing plugin');
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// TEST327: PluginRepoServer search plugins
|
|
2130
|
+
function test327_pluginRepoServerSearchPlugins() {
|
|
2131
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2132
|
+
|
|
2133
|
+
const pdfResults = server.searchPlugins('pdf');
|
|
2134
|
+
assert(pdfResults.length === 1, 'Should find 1 PDF plugin');
|
|
2135
|
+
assert(pdfResults[0].id === 'pdfcartridge', 'Should find pdfcartridge');
|
|
2136
|
+
|
|
2137
|
+
const metadataResults = server.searchPlugins('metadata');
|
|
2138
|
+
assert(metadataResults.length === 1, 'Should find plugin by cap title');
|
|
2139
|
+
|
|
2140
|
+
const noResults = server.searchPlugins('nonexistent');
|
|
2141
|
+
assert(noResults.length === 0, 'Should return empty for no matches');
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// TEST328: PluginRepoServer get by category
|
|
2145
|
+
function test328_pluginRepoServerGetByCategory() {
|
|
2146
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2147
|
+
|
|
2148
|
+
const docPlugins = server.getPluginsByCategory('document');
|
|
2149
|
+
assert(docPlugins.length === 1, 'Should find 1 document plugin');
|
|
2150
|
+
assert(docPlugins[0].id === 'pdfcartridge', 'Should be pdfcartridge');
|
|
2151
|
+
|
|
2152
|
+
const textPlugins = server.getPluginsByCategory('text');
|
|
2153
|
+
assert(textPlugins.length === 1, 'Should find 1 text plugin');
|
|
2154
|
+
assert(textPlugins[0].id === 'txtcartridge', 'Should be txtcartridge');
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// TEST329: PluginRepoServer get by cap
|
|
2158
|
+
function test329_pluginRepoServerGetByCap() {
|
|
2159
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2160
|
+
|
|
2161
|
+
const disbindCap = 'cap:in="media:pdf";op=disbind;out="media:disbound-page;textable;list"';
|
|
2162
|
+
const plugins = server.getPluginsByCap(disbindCap);
|
|
2163
|
+
|
|
2164
|
+
assert(plugins.length === 1, 'Should find 1 plugin with this cap');
|
|
2165
|
+
assert(plugins[0].id === 'pdfcartridge', 'Should be pdfcartridge');
|
|
2166
|
+
|
|
2167
|
+
const metadataCap = 'cap:in="media:pdf";op=extract_metadata;out="media:file-metadata;textable;record"';
|
|
2168
|
+
const metadataPlugins = server.getPluginsByCap(metadataCap);
|
|
2169
|
+
assert(metadataPlugins.length === 1, 'Should find metadata cap');
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// TEST330: PluginRepoClient update cache
|
|
2173
|
+
function test330_pluginRepoClientUpdateCache() {
|
|
2174
|
+
const client = new PluginRepoClient(3600);
|
|
2175
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2176
|
+
const plugins = server.transformToPluginArray().map(p => new PluginInfo(p));
|
|
2177
|
+
|
|
2178
|
+
client.updateCache('https://example.com/api/plugins', plugins);
|
|
2179
|
+
|
|
2180
|
+
const cache = client.caches.get('https://example.com/api/plugins');
|
|
2181
|
+
assert(cache !== undefined, 'Cache should exist');
|
|
2182
|
+
assert(cache.plugins.size === 2, 'Should have 2 plugins in cache');
|
|
2183
|
+
assert(cache.capToPlugins.size > 0, 'Should have cap mappings');
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// TEST331: PluginRepoClient get suggestions
|
|
2187
|
+
function test331_pluginRepoClientGetSuggestions() {
|
|
2188
|
+
const client = new PluginRepoClient(3600);
|
|
2189
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2190
|
+
const plugins = server.transformToPluginArray().map(p => new PluginInfo(p));
|
|
2191
|
+
|
|
2192
|
+
client.updateCache('https://example.com/api/plugins', plugins);
|
|
2193
|
+
|
|
2194
|
+
const disbindCap = 'cap:in="media:pdf";op=disbind;out="media:disbound-page;textable;list"';
|
|
2195
|
+
const suggestions = client.getSuggestionsForCap(disbindCap);
|
|
2196
|
+
|
|
2197
|
+
assert(suggestions.length === 1, 'Should find 1 suggestion');
|
|
2198
|
+
assert(suggestions[0].pluginId === 'pdfcartridge', 'Should suggest pdfcartridge');
|
|
2199
|
+
assert(suggestions[0].capUrn === disbindCap, 'Should have correct cap URN');
|
|
2200
|
+
assert(suggestions[0].capTitle === 'Disbind PDF', 'Should have cap title');
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// TEST332: PluginRepoClient get plugin
|
|
2204
|
+
function test332_pluginRepoClientGetPlugin() {
|
|
2205
|
+
const client = new PluginRepoClient(3600);
|
|
2206
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2207
|
+
const plugins = server.transformToPluginArray().map(p => new PluginInfo(p));
|
|
2208
|
+
|
|
2209
|
+
client.updateCache('https://example.com/api/plugins', plugins);
|
|
2210
|
+
|
|
2211
|
+
const plugin = client.getPlugin('pdfcartridge');
|
|
2212
|
+
assert(plugin !== null, 'Should find plugin');
|
|
2213
|
+
assert(plugin.id === 'pdfcartridge', 'Should have correct ID');
|
|
2214
|
+
|
|
2215
|
+
const notFound = client.getPlugin('nonexistent');
|
|
2216
|
+
assert(notFound === null, 'Should return null for missing plugin');
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// TEST333: PluginRepoClient get all caps
|
|
2220
|
+
function test333_pluginRepoClientGetAllCaps() {
|
|
2221
|
+
const client = new PluginRepoClient(3600);
|
|
2222
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2223
|
+
const plugins = server.transformToPluginArray().map(p => new PluginInfo(p));
|
|
2224
|
+
|
|
2225
|
+
client.updateCache('https://example.com/api/plugins', plugins);
|
|
2226
|
+
|
|
2227
|
+
const caps = client.getAllAvailableCaps();
|
|
2228
|
+
assert(Array.isArray(caps), 'Should return array');
|
|
2229
|
+
assert(caps.length === 3, 'Should have 3 unique caps');
|
|
2230
|
+
assert(caps.every(c => typeof c === 'string'), 'All caps should be strings');
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// TEST334: PluginRepoClient needs sync
|
|
2234
|
+
function test334_pluginRepoClientNeedsSync() {
|
|
2235
|
+
const client = new PluginRepoClient(1); // 1 second TTL
|
|
2236
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2237
|
+
const plugins = server.transformToPluginArray().map(p => new PluginInfo(p));
|
|
2238
|
+
|
|
2239
|
+
const urls = ['https://example.com/api/plugins'];
|
|
2240
|
+
|
|
2241
|
+
// Should need sync initially
|
|
2242
|
+
assert(client.needsSync(urls) === true, 'Should need sync with empty cache');
|
|
2243
|
+
|
|
2244
|
+
// Update cache
|
|
2245
|
+
client.updateCache(urls[0], plugins);
|
|
2246
|
+
|
|
2247
|
+
// Should not need sync immediately
|
|
2248
|
+
assert(client.needsSync(urls) === false, 'Should not need sync right after update');
|
|
2249
|
+
|
|
2250
|
+
// Wait for cache to expire (1 second)
|
|
2251
|
+
// Note: Can't test this synchronously, would need async test
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// TEST335: PluginRepoServer and Client integration
|
|
2255
|
+
function test335_pluginRepoServerClientIntegration() {
|
|
2256
|
+
// Server creates API response
|
|
2257
|
+
const server = new PluginRepoServer(sampleRegistry);
|
|
2258
|
+
const apiResponse = server.getPlugins();
|
|
2259
|
+
|
|
2260
|
+
// Client consumes API response
|
|
2261
|
+
const client = new PluginRepoClient(3600);
|
|
2262
|
+
const plugins = apiResponse.plugins.map(p => new PluginInfo(p));
|
|
2263
|
+
client.updateCache('https://example.com/api/plugins', plugins);
|
|
2264
|
+
|
|
2265
|
+
// Client can find plugin
|
|
2266
|
+
const plugin = client.getPlugin('pdfcartridge');
|
|
2267
|
+
assert(plugin !== null, 'Client should find plugin from server data');
|
|
2268
|
+
assert(plugin.isSigned(), 'Plugin should be signed');
|
|
2269
|
+
assert(plugin.hasBinary(), 'Plugin should have binary');
|
|
2270
|
+
|
|
2271
|
+
// Client can get suggestions
|
|
2272
|
+
const capUrn = 'cap:in="media:pdf";op=disbind;out="media:disbound-page;textable;list"';
|
|
2273
|
+
const suggestions = client.getSuggestionsForCap(capUrn);
|
|
2274
|
+
assert(suggestions.length === 1, 'Should get suggestions');
|
|
2275
|
+
assert(suggestions[0].pluginId === 'pdfcartridge', 'Should suggest correct plugin');
|
|
2276
|
+
|
|
2277
|
+
// Server can search
|
|
2278
|
+
const searchResults = server.searchPlugins('pdf');
|
|
2279
|
+
assert(searchResults.length === 1, 'Server search should work');
|
|
2280
|
+
assert(searchResults[0].id === plugin.id, 'Search and client should agree');
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// ============================================================================
|
|
2284
|
+
// media_urn.rs: TEST546-TEST558 (MediaUrn predicates)
|
|
2285
|
+
// ============================================================================
|
|
2286
|
+
|
|
2287
|
+
// TEST546: isImage returns true only when image marker tag is present
|
|
2288
|
+
function test546_isImage() {
|
|
2289
|
+
assert(MediaUrn.fromString(MEDIA_PNG).isImage(), 'MEDIA_PNG should be image');
|
|
2290
|
+
assert(MediaUrn.fromString(MEDIA_IMAGE_THUMBNAIL).isImage(), 'MEDIA_IMAGE_THUMBNAIL should be image');
|
|
2291
|
+
assert(MediaUrn.fromString('media:image;jpg').isImage(), 'media:image;jpg should be image');
|
|
2292
|
+
// Non-image types
|
|
2293
|
+
assert(!MediaUrn.fromString(MEDIA_PDF).isImage(), 'MEDIA_PDF should not be image');
|
|
2294
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isImage(), 'MEDIA_STRING should not be image');
|
|
2295
|
+
assert(!MediaUrn.fromString(MEDIA_AUDIO).isImage(), 'MEDIA_AUDIO should not be image');
|
|
2296
|
+
assert(!MediaUrn.fromString(MEDIA_VIDEO).isImage(), 'MEDIA_VIDEO should not be image');
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// TEST547: isAudio returns true only when audio marker tag is present
|
|
2300
|
+
function test547_isAudio() {
|
|
2301
|
+
assert(MediaUrn.fromString(MEDIA_AUDIO).isAudio(), 'MEDIA_AUDIO should be audio');
|
|
2302
|
+
assert(MediaUrn.fromString(MEDIA_AUDIO_SPEECH).isAudio(), 'MEDIA_AUDIO_SPEECH should be audio');
|
|
2303
|
+
assert(MediaUrn.fromString('media:audio;mp3').isAudio(), 'media:audio;mp3 should be audio');
|
|
2304
|
+
// Non-audio types
|
|
2305
|
+
assert(!MediaUrn.fromString(MEDIA_VIDEO).isAudio(), 'MEDIA_VIDEO should not be audio');
|
|
2306
|
+
assert(!MediaUrn.fromString(MEDIA_PNG).isAudio(), 'MEDIA_PNG should not be audio');
|
|
2307
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isAudio(), 'MEDIA_STRING should not be audio');
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// TEST548: isVideo returns true only when video marker tag is present
|
|
2311
|
+
function test548_isVideo() {
|
|
2312
|
+
assert(MediaUrn.fromString(MEDIA_VIDEO).isVideo(), 'MEDIA_VIDEO should be video');
|
|
2313
|
+
assert(MediaUrn.fromString('media:video;mp4').isVideo(), 'media:video;mp4 should be video');
|
|
2314
|
+
// Non-video types
|
|
2315
|
+
assert(!MediaUrn.fromString(MEDIA_AUDIO).isVideo(), 'MEDIA_AUDIO should not be video');
|
|
2316
|
+
assert(!MediaUrn.fromString(MEDIA_PNG).isVideo(), 'MEDIA_PNG should not be video');
|
|
2317
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isVideo(), 'MEDIA_STRING should not be video');
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// TEST549: isNumeric returns true only when numeric marker tag is present
|
|
2321
|
+
function test549_isNumeric() {
|
|
2322
|
+
assert(MediaUrn.fromString(MEDIA_INTEGER).isNumeric(), 'MEDIA_INTEGER should be numeric');
|
|
2323
|
+
assert(MediaUrn.fromString(MEDIA_NUMBER).isNumeric(), 'MEDIA_NUMBER should be numeric');
|
|
2324
|
+
assert(MediaUrn.fromString(MEDIA_INTEGER_ARRAY).isNumeric(), 'MEDIA_INTEGER_ARRAY should be numeric');
|
|
2325
|
+
assert(MediaUrn.fromString(MEDIA_NUMBER_ARRAY).isNumeric(), 'MEDIA_NUMBER_ARRAY should be numeric');
|
|
2326
|
+
// Non-numeric types
|
|
2327
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isNumeric(), 'MEDIA_STRING should not be numeric');
|
|
2328
|
+
assert(!MediaUrn.fromString(MEDIA_BOOLEAN).isNumeric(), 'MEDIA_BOOLEAN should not be numeric');
|
|
2329
|
+
assert(!MediaUrn.fromString(MEDIA_BINARY).isNumeric(), 'MEDIA_BINARY should not be numeric');
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// TEST550: isBool returns true only when bool marker tag is present
|
|
2333
|
+
function test550_isBool() {
|
|
2334
|
+
assert(MediaUrn.fromString(MEDIA_BOOLEAN).isBool(), 'MEDIA_BOOLEAN should be bool');
|
|
2335
|
+
assert(MediaUrn.fromString(MEDIA_BOOLEAN_ARRAY).isBool(), 'MEDIA_BOOLEAN_ARRAY should be bool');
|
|
2336
|
+
assert(MediaUrn.fromString(MEDIA_DECISION).isBool(), 'MEDIA_DECISION should be bool');
|
|
2337
|
+
assert(MediaUrn.fromString(MEDIA_DECISION_ARRAY).isBool(), 'MEDIA_DECISION_ARRAY should be bool');
|
|
2338
|
+
// Non-bool types
|
|
2339
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isBool(), 'MEDIA_STRING should not be bool');
|
|
2340
|
+
assert(!MediaUrn.fromString(MEDIA_INTEGER).isBool(), 'MEDIA_INTEGER should not be bool');
|
|
2341
|
+
assert(!MediaUrn.fromString(MEDIA_BINARY).isBool(), 'MEDIA_BINARY should not be bool');
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
// TEST551: isFilePath returns true for scalar file-path, false for array
|
|
2345
|
+
function test551_isFilePath() {
|
|
2346
|
+
assert(MediaUrn.fromString(MEDIA_FILE_PATH).isFilePath(), 'MEDIA_FILE_PATH should be file-path');
|
|
2347
|
+
// Array file-path is NOT isFilePath (it's isFilePathArray)
|
|
2348
|
+
assert(!MediaUrn.fromString(MEDIA_FILE_PATH_ARRAY).isFilePath(), 'MEDIA_FILE_PATH_ARRAY should not be isFilePath');
|
|
2349
|
+
// Non-file-path types
|
|
2350
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isFilePath(), 'MEDIA_STRING should not be file-path');
|
|
2351
|
+
assert(!MediaUrn.fromString(MEDIA_BINARY).isFilePath(), 'MEDIA_BINARY should not be file-path');
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// TEST552: isFilePathArray returns true for list file-path, false for scalar
|
|
2355
|
+
function test552_isFilePathArray() {
|
|
2356
|
+
assert(MediaUrn.fromString(MEDIA_FILE_PATH_ARRAY).isFilePathArray(), 'MEDIA_FILE_PATH_ARRAY should be file-path-array');
|
|
2357
|
+
// Scalar file-path is NOT isFilePathArray
|
|
2358
|
+
assert(!MediaUrn.fromString(MEDIA_FILE_PATH).isFilePathArray(), 'MEDIA_FILE_PATH should not be isFilePathArray');
|
|
2359
|
+
// Non-file-path types
|
|
2360
|
+
assert(!MediaUrn.fromString(MEDIA_STRING_ARRAY).isFilePathArray(), 'MEDIA_STRING_ARRAY should not be file-path-array');
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// TEST553: isAnyFilePath returns true for both scalar and array file-path
|
|
2364
|
+
function test553_isAnyFilePath() {
|
|
2365
|
+
assert(MediaUrn.fromString(MEDIA_FILE_PATH).isAnyFilePath(), 'MEDIA_FILE_PATH should be any-file-path');
|
|
2366
|
+
assert(MediaUrn.fromString(MEDIA_FILE_PATH_ARRAY).isAnyFilePath(), 'MEDIA_FILE_PATH_ARRAY should be any-file-path');
|
|
2367
|
+
// Non-file-path types
|
|
2368
|
+
assert(!MediaUrn.fromString(MEDIA_STRING).isAnyFilePath(), 'MEDIA_STRING should not be any-file-path');
|
|
2369
|
+
assert(!MediaUrn.fromString(MEDIA_STRING_ARRAY).isAnyFilePath(), 'MEDIA_STRING_ARRAY should not be any-file-path');
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// TEST554: isCollection returns true when collection marker tag is present
|
|
2373
|
+
// TEST554: N/A for JS (MEDIA_COLLECTION constants removed - no longer exists)
|
|
2374
|
+
function test554_isCollection() {
|
|
2375
|
+
// Skip - collection types removed from capdag
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// TEST555: N/A for JS (with_tag/without_tag on MediaUrn - JS MediaUrn does not have these methods)
|
|
2379
|
+
|
|
2380
|
+
// TEST556: N/A for JS (image_media_urn_for_ext helper not in JS)
|
|
2381
|
+
|
|
2382
|
+
// TEST557: N/A for JS (audio_media_urn_for_ext helper not in JS)
|
|
2383
|
+
|
|
2384
|
+
// TEST558: predicates are consistent with constants - every constant triggers exactly the expected predicates
|
|
2385
|
+
function test558_predicateConstantConsistency() {
|
|
2386
|
+
// MEDIA_INTEGER must be numeric, text, scalar, NOT binary/bool/image/audio/video
|
|
2387
|
+
const intUrn = MediaUrn.fromString(MEDIA_INTEGER);
|
|
2388
|
+
assert(intUrn.isNumeric(), 'MEDIA_INTEGER must be numeric');
|
|
2389
|
+
assert(intUrn.isText(), 'MEDIA_INTEGER must be text');
|
|
2390
|
+
assert(intUrn.isScalar(), 'MEDIA_INTEGER must be scalar');
|
|
2391
|
+
assert(!intUrn.isBinary(), 'MEDIA_INTEGER must not be binary');
|
|
2392
|
+
assert(!intUrn.isBool(), 'MEDIA_INTEGER must not be bool');
|
|
2393
|
+
assert(!intUrn.isImage(), 'MEDIA_INTEGER must not be image');
|
|
2394
|
+
assert(!intUrn.isList(), 'MEDIA_INTEGER must not be list');
|
|
2395
|
+
|
|
2396
|
+
// MEDIA_BOOLEAN must be bool, text, scalar, NOT numeric
|
|
2397
|
+
const boolUrn = MediaUrn.fromString(MEDIA_BOOLEAN);
|
|
2398
|
+
assert(boolUrn.isBool(), 'MEDIA_BOOLEAN must be bool');
|
|
2399
|
+
assert(boolUrn.isText(), 'MEDIA_BOOLEAN must be text');
|
|
2400
|
+
assert(boolUrn.isScalar(), 'MEDIA_BOOLEAN must be scalar');
|
|
2401
|
+
assert(!boolUrn.isNumeric(), 'MEDIA_BOOLEAN must not be numeric');
|
|
2402
|
+
|
|
2403
|
+
// MEDIA_JSON must be json, text, record, scalar, NOT binary
|
|
2404
|
+
const jsonUrn = MediaUrn.fromString(MEDIA_JSON);
|
|
2405
|
+
assert(jsonUrn.isJson(), 'MEDIA_JSON must be json');
|
|
2406
|
+
assert(jsonUrn.isText(), 'MEDIA_JSON must be text');
|
|
2407
|
+
assert(jsonUrn.isRecord(), 'MEDIA_JSON must be record');
|
|
2408
|
+
assert(jsonUrn.isScalar(), 'MEDIA_JSON must be scalar (no list marker)');
|
|
2409
|
+
assert(!jsonUrn.isBinary(), 'MEDIA_JSON must not be binary');
|
|
2410
|
+
assert(!jsonUrn.isList(), 'MEDIA_JSON must not be list');
|
|
2411
|
+
|
|
2412
|
+
// MEDIA_VOID is void, NOT text/numeric — but IS binary (no textable tag)
|
|
2413
|
+
const voidUrn = MediaUrn.fromString(MEDIA_VOID);
|
|
2414
|
+
assert(voidUrn.isVoid(), 'MEDIA_VOID must be void');
|
|
2415
|
+
assert(!voidUrn.isText(), 'MEDIA_VOID must not be text');
|
|
2416
|
+
assert(voidUrn.isBinary(), 'MEDIA_VOID must be binary (no textable tag)');
|
|
2417
|
+
assert(!voidUrn.isNumeric(), 'MEDIA_VOID must not be numeric');
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// ============================================================================
|
|
2421
|
+
// cap_urn.rs: TEST559-TEST567 (CapUrn tier tests)
|
|
2422
|
+
// ============================================================================
|
|
2423
|
+
|
|
2424
|
+
// TEST559: withoutTag removes tag, ignores in/out, case-insensitive for keys
|
|
2425
|
+
function test559_withoutTag() {
|
|
2426
|
+
const cap = CapUrn.fromString('cap:in="media:void";op=test;ext=pdf;out="media:void"');
|
|
2427
|
+
const removed = cap.withoutTag('ext');
|
|
2428
|
+
assertEqual(removed.getTag('ext'), undefined, 'withoutTag should remove ext');
|
|
2429
|
+
assertEqual(removed.getTag('op'), 'test', 'withoutTag should preserve op');
|
|
2430
|
+
|
|
2431
|
+
// Case-insensitive removal
|
|
2432
|
+
const removed2 = cap.withoutTag('EXT');
|
|
2433
|
+
assertEqual(removed2.getTag('ext'), undefined, 'withoutTag should be case-insensitive');
|
|
2434
|
+
|
|
2435
|
+
// Removing in/out is silently ignored
|
|
2436
|
+
const same = cap.withoutTag('in');
|
|
2437
|
+
assertEqual(same.getInSpec(), 'media:void', 'withoutTag must not remove in');
|
|
2438
|
+
const same2 = cap.withoutTag('out');
|
|
2439
|
+
assertEqual(same2.getOutSpec(), 'media:void', 'withoutTag must not remove out');
|
|
2440
|
+
|
|
2441
|
+
// Removing non-existent tag is no-op
|
|
2442
|
+
const same3 = cap.withoutTag('nonexistent');
|
|
2443
|
+
assert(same3.equals(cap), 'Removing non-existent tag is no-op');
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// TEST560: withInSpec and withOutSpec change direction specs
|
|
2447
|
+
function test560_withInOutSpec() {
|
|
2448
|
+
const cap = CapUrn.fromString('cap:in="media:void";op=test;out="media:void"');
|
|
2449
|
+
|
|
2450
|
+
const changedIn = cap.withInSpec('media:');
|
|
2451
|
+
assertEqual(changedIn.getInSpec(), 'media:', 'withInSpec should change inSpec');
|
|
2452
|
+
assertEqual(changedIn.getOutSpec(), 'media:void', 'withInSpec should preserve outSpec');
|
|
2453
|
+
assertEqual(changedIn.getTag('op'), 'test', 'withInSpec should preserve tags');
|
|
2454
|
+
|
|
2455
|
+
const changedOut = cap.withOutSpec('media:string');
|
|
2456
|
+
assertEqual(changedOut.getInSpec(), 'media:void', 'withOutSpec should preserve inSpec');
|
|
2457
|
+
assertEqual(changedOut.getOutSpec(), 'media:string', 'withOutSpec should change outSpec');
|
|
2458
|
+
|
|
2459
|
+
// Chain both
|
|
2460
|
+
const changedBoth = cap.withInSpec('media:pdf').withOutSpec('media:txt;textable');
|
|
2461
|
+
assertEqual(changedBoth.getInSpec(), 'media:pdf', 'Chain should set inSpec');
|
|
2462
|
+
assertEqual(changedBoth.getOutSpec(), 'media:txt;textable', 'Chain should set outSpec');
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// TEST561: N/A for JS (in_media_urn/out_media_urn not in JS CapUrn)
|
|
2466
|
+
|
|
2467
|
+
// TEST562: N/A for JS (canonical_option not in JS CapUrn)
|
|
2468
|
+
|
|
2469
|
+
// TEST563: CapMatcher.findAllMatches returns all matching caps sorted by specificity
|
|
2470
|
+
function test563_findAllMatches() {
|
|
2471
|
+
const caps = [
|
|
2472
|
+
CapUrn.fromString('cap:in="media:void";op=test;out="media:void"'),
|
|
2473
|
+
CapUrn.fromString('cap:in="media:void";op=test;ext=pdf;out="media:void"'),
|
|
2474
|
+
CapUrn.fromString('cap:in="media:void";op=different;out="media:void"'),
|
|
2475
|
+
];
|
|
2476
|
+
|
|
2477
|
+
const request = CapUrn.fromString('cap:in="media:void";op=test;out="media:void"');
|
|
2478
|
+
const matches = CapMatcher.findAllMatches(caps, request);
|
|
2479
|
+
|
|
2480
|
+
// Should find 2 matches (op=test and op=test;ext=pdf), not op=different
|
|
2481
|
+
assertEqual(matches.length, 2, 'Should find 2 matches');
|
|
2482
|
+
// Sorted by specificity descending: ext=pdf first (more specific)
|
|
2483
|
+
assert(matches[0].specificity() >= matches[1].specificity(), 'First match should be more specific');
|
|
2484
|
+
assertEqual(matches[0].getTag('ext'), 'pdf', 'Most specific match should have ext=pdf');
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// TEST564: CapMatcher.areCompatible detects bidirectional overlap
|
|
2488
|
+
function test564_areCompatible() {
|
|
2489
|
+
const caps1 = [
|
|
2490
|
+
CapUrn.fromString('cap:in="media:void";op=test;out="media:void"'),
|
|
2491
|
+
];
|
|
2492
|
+
const caps2 = [
|
|
2493
|
+
CapUrn.fromString('cap:in="media:void";op=test;ext=pdf;out="media:void"'),
|
|
2494
|
+
];
|
|
2495
|
+
const caps3 = [
|
|
2496
|
+
CapUrn.fromString('cap:in="media:void";op=different;out="media:void"'),
|
|
2497
|
+
];
|
|
2498
|
+
|
|
2499
|
+
// caps1 (op=test) accepts caps2 (op=test;ext=pdf) -> compatible
|
|
2500
|
+
assert(CapMatcher.areCompatible(caps1, caps2), 'caps1 and caps2 should be compatible');
|
|
2501
|
+
|
|
2502
|
+
// caps1 (op=test) vs caps3 (op=different) -> not compatible
|
|
2503
|
+
assert(!CapMatcher.areCompatible(caps1, caps3), 'caps1 and caps3 should not be compatible');
|
|
2504
|
+
|
|
2505
|
+
// Empty sets are not compatible
|
|
2506
|
+
assert(!CapMatcher.areCompatible([], caps1), 'Empty vs non-empty should not be compatible');
|
|
2507
|
+
assert(!CapMatcher.areCompatible(caps1, []), 'Non-empty vs empty should not be compatible');
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
// TEST565: N/A for JS (tags_to_string not in JS CapUrn)
|
|
2511
|
+
|
|
2512
|
+
// TEST566: withTag silently ignores in/out keys
|
|
2513
|
+
function test566_withTagIgnoresInOut() {
|
|
2514
|
+
const cap = CapUrn.fromString('cap:in="media:void";op=test;out="media:void"');
|
|
2515
|
+
// Attempting to set in/out via withTag is silently ignored
|
|
2516
|
+
const same = cap.withTag('in', 'media:');
|
|
2517
|
+
assertEqual(same.getInSpec(), 'media:void', 'withTag must not change in_spec');
|
|
2518
|
+
|
|
2519
|
+
const same2 = cap.withTag('out', 'media:');
|
|
2520
|
+
assertEqual(same2.getOutSpec(), 'media:void', 'withTag must not change out_spec');
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// TEST567: N/A for JS (conforms_to_str/accepts_str not in JS CapUrn)
|
|
2524
|
+
|
|
2525
|
+
// ============================================================================
|
|
2526
|
+
// cap_urn.rs: TEST639-TEST653 (Cap URN wildcard tests)
|
|
2527
|
+
// ============================================================================
|
|
2528
|
+
|
|
2529
|
+
// Note: Rust allows missing in/out to default to "media:" wildcard.
|
|
2530
|
+
// JS currently requires in/out (throws MISSING_IN_SPEC/MISSING_OUT_SPEC).
|
|
2531
|
+
// The following tests cover the wildcard behavior that IS applicable to JS.
|
|
2532
|
+
|
|
2533
|
+
// TEST639-642: N/A for JS (JS requires in/out, does not default to media: wildcard)
|
|
2534
|
+
|
|
2535
|
+
// TEST643: cap:in=*;out=* treated as wildcards
|
|
2536
|
+
function test643_explicitAsteriskIsWildcard() {
|
|
2537
|
+
const cap = CapUrn.fromString('cap:in=*;out=*');
|
|
2538
|
+
assertEqual(cap.getInSpec(), '*', 'in=* should be stored as wildcard');
|
|
2539
|
+
assertEqual(cap.getOutSpec(), '*', 'out=* should be stored as wildcard');
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// TEST644: cap:in=media:;out=* has specific in, wildcard out
|
|
2543
|
+
function test644_specificInWildcardOut() {
|
|
2544
|
+
const cap = CapUrn.fromString('cap:in=media:;out=*');
|
|
2545
|
+
assertEqual(cap.getInSpec(), 'media:', 'Should have specific in');
|
|
2546
|
+
assertEqual(cap.getOutSpec(), '*', 'Should have wildcard out');
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// TEST645: cap:in=*;out=media:text has wildcard in, specific out
|
|
2550
|
+
function test645_wildcardInSpecificOut() {
|
|
2551
|
+
const cap = CapUrn.fromString('cap:in=*;out=media:text');
|
|
2552
|
+
assertEqual(cap.getInSpec(), '*', 'Should have wildcard in');
|
|
2553
|
+
assertEqual(cap.getOutSpec(), 'media:text', 'Should have specific out');
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// TEST646: N/A for JS (JS allows in=foo since it just checks for media: or *)
|
|
2557
|
+
// TEST647: N/A for JS (JS allows out=bar since it just checks for media: or *)
|
|
2558
|
+
|
|
2559
|
+
// TEST648: Wildcard in/out match specific caps
|
|
2560
|
+
function test648_wildcardAcceptsSpecific() {
|
|
2561
|
+
const wildcard = CapUrn.fromString('cap:in=*;out=*');
|
|
2562
|
+
const specific = CapUrn.fromString('cap:in="media:";out="media:text"');
|
|
2563
|
+
|
|
2564
|
+
assert(wildcard.accepts(specific), 'Wildcard should accept specific');
|
|
2565
|
+
assert(specific.conformsTo(wildcard), 'Specific should conform to wildcard');
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// TEST649: Specificity - wildcard has 0, specific has tag count
|
|
2569
|
+
function test649_specificityScoring() {
|
|
2570
|
+
const wildcard = CapUrn.fromString('cap:in=*;out=*');
|
|
2571
|
+
const specific = CapUrn.fromString('cap:in="media:";out="media:text"');
|
|
2572
|
+
|
|
2573
|
+
assertEqual(wildcard.specificity(), 0, 'Wildcard cap should have 0 specificity');
|
|
2574
|
+
assert(specific.specificity() > 0, 'Specific cap should have non-zero specificity');
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// TEST650: N/A for JS (JS requires in/out, cap:in;out;op=test would fail parsing)
|
|
2578
|
+
|
|
2579
|
+
// TEST651: All identity forms with explicit wildcards produce the same CapUrn
|
|
2580
|
+
function test651_identityFormsEquivalent() {
|
|
2581
|
+
const forms = [
|
|
2582
|
+
'cap:in=*;out=*',
|
|
2583
|
+
'cap:in="media:";out="media:"',
|
|
2584
|
+
];
|
|
2585
|
+
|
|
2586
|
+
const first = CapUrn.fromString(forms[0]);
|
|
2587
|
+
// All forms should produce equivalent caps (wildcard behavior)
|
|
2588
|
+
for (let i = 1; i < forms.length; i++) {
|
|
2589
|
+
const cap = CapUrn.fromString(forms[i]);
|
|
2590
|
+
// Both should accept specific caps
|
|
2591
|
+
const specific = CapUrn.fromString('cap:in="media:";out="media:text"');
|
|
2592
|
+
assert(first.accepts(specific), `Form 0 should accept specific`);
|
|
2593
|
+
assert(cap.accepts(specific), `Form ${i} should accept specific`);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// TEST652: N/A for JS (CAP_IDENTITY constant not in JS)
|
|
2598
|
+
|
|
2599
|
+
// TEST653: Identity (no extra tags) does not steal routes from specific handlers
|
|
2600
|
+
function test653_identityRoutingIsolation() {
|
|
2601
|
+
const identity = CapUrn.fromString('cap:in=*;out=*');
|
|
2602
|
+
const specificRequest = CapUrn.fromString('cap:in="media:void";op=test;out="media:void"');
|
|
2603
|
+
|
|
2604
|
+
// Identity has specificity 0 (no tags, wildcard directions)
|
|
2605
|
+
assertEqual(identity.specificity(), 0, 'Identity specificity should be 0');
|
|
2606
|
+
|
|
2607
|
+
// Specific request has higher specificity
|
|
2608
|
+
assert(specificRequest.specificity() > identity.specificity(),
|
|
2609
|
+
'Specific request should have higher specificity than identity');
|
|
2610
|
+
|
|
2611
|
+
// CapMatcher should prefer specific over identity
|
|
2612
|
+
const specificCap = CapUrn.fromString('cap:in="media:void";op=test;out="media:void"');
|
|
2613
|
+
const best = CapMatcher.findBestMatch([identity, specificCap], specificRequest);
|
|
2614
|
+
assert(best !== null, 'Should find a match');
|
|
2615
|
+
assertEqual(best.getTag('op'), 'test', 'CapMatcher should prefer specific cap over identity');
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// ============================================================================
|
|
2619
|
+
// Test runner
|
|
2620
|
+
// ============================================================================
|
|
2621
|
+
|
|
2622
|
+
async function runTests() {
|
|
2623
|
+
console.log('Running capdag-js tests...\n');
|
|
2624
|
+
|
|
2625
|
+
// cap_urn.rs: TEST001-TEST050, TEST890-TEST891
|
|
2626
|
+
console.log('--- cap_urn.rs ---');
|
|
2627
|
+
runTest('TEST001: cap_urn_creation', test001_capUrnCreation);
|
|
2628
|
+
runTest('TEST002: direction_specs_required', test002_directionSpecsRequired);
|
|
2629
|
+
runTest('TEST003: direction_matching', test003_directionMatching);
|
|
2630
|
+
runTest('TEST004: unquoted_values_lowercased', test004_unquotedValuesLowercased);
|
|
2631
|
+
runTest('TEST005: quoted_values_preserve_case', test005_quotedValuesPreserveCase);
|
|
2632
|
+
runTest('TEST006: quoted_value_special_chars', test006_quotedValueSpecialChars);
|
|
2633
|
+
runTest('TEST007: quoted_value_escape_sequences', test007_quotedValueEscapeSequences);
|
|
2634
|
+
runTest('TEST008: mixed_quoted_unquoted', test008_mixedQuotedUnquoted);
|
|
2635
|
+
runTest('TEST009: unterminated_quote_error', test009_unterminatedQuoteError);
|
|
2636
|
+
runTest('TEST010: invalid_escape_sequence_error', test010_invalidEscapeSequenceError);
|
|
2637
|
+
runTest('TEST011: serialization_smart_quoting', test011_serializationSmartQuoting);
|
|
2638
|
+
runTest('TEST012: round_trip_simple', test012_roundTripSimple);
|
|
2639
|
+
runTest('TEST013: round_trip_quoted', test013_roundTripQuoted);
|
|
2640
|
+
runTest('TEST014: round_trip_escapes', test014_roundTripEscapes);
|
|
2641
|
+
runTest('TEST015: cap_prefix_required', test015_capPrefixRequired);
|
|
2642
|
+
runTest('TEST016: trailing_semicolon_equivalence', test016_trailingSemicolonEquivalence);
|
|
2643
|
+
runTest('TEST017: tag_matching', test017_tagMatching);
|
|
2644
|
+
runTest('TEST018: matching_case_sensitive_values', test018_matchingCaseSensitiveValues);
|
|
2645
|
+
runTest('TEST019: missing_tag_handling', test019_missingTagHandling);
|
|
2646
|
+
runTest('TEST020: specificity', test020_specificity);
|
|
2647
|
+
runTest('TEST021: builder', test021_builder);
|
|
2648
|
+
runTest('TEST022: builder_requires_direction', test022_builderRequiresDirection);
|
|
2649
|
+
runTest('TEST023: builder_preserves_case', test023_builderPreservesCase);
|
|
2650
|
+
runTest('TEST024: compatibility', test024_compatibility);
|
|
2651
|
+
runTest('TEST025: best_match', test025_bestMatch);
|
|
2652
|
+
runTest('TEST026: merge_and_subset', test026_mergeAndSubset);
|
|
2653
|
+
runTest('TEST027: wildcard_tag', test027_wildcardTag);
|
|
2654
|
+
runTest('TEST028: empty_cap_urn_not_allowed', test028_emptyCapUrnNotAllowed);
|
|
2655
|
+
runTest('TEST029: minimal_cap_urn', test029_minimalCapUrn);
|
|
2656
|
+
runTest('TEST030: extended_character_support', test030_extendedCharacterSupport);
|
|
2657
|
+
runTest('TEST031: wildcard_restrictions', test031_wildcardRestrictions);
|
|
2658
|
+
runTest('TEST032: duplicate_key_rejection', test032_duplicateKeyRejection);
|
|
2659
|
+
runTest('TEST033: numeric_key_restriction', test033_numericKeyRestriction);
|
|
2660
|
+
runTest('TEST034: empty_value_error', test034_emptyValueError);
|
|
2661
|
+
runTest('TEST035: has_tag_case_sensitive', test035_hasTagCaseSensitive);
|
|
2662
|
+
runTest('TEST036: with_tag_preserves_value', test036_withTagPreservesValue);
|
|
2663
|
+
runTest('TEST037: with_tag_rejects_empty_value', test037_withTagRejectsEmptyValue);
|
|
2664
|
+
runTest('TEST038: semantic_equivalence', test038_semanticEquivalence);
|
|
2665
|
+
runTest('TEST039: get_tag_returns_direction_specs', test039_getTagReturnsDirectionSpecs);
|
|
2666
|
+
runTest('TEST040: matching_semantics_exact_match', test040_matchingSemanticsExactMatch);
|
|
2667
|
+
runTest('TEST041: matching_semantics_cap_missing_tag', test041_matchingSemanticsCapMissingTag);
|
|
2668
|
+
runTest('TEST042: matching_semantics_cap_has_extra_tag', test042_matchingSemanticsCapHasExtraTag);
|
|
2669
|
+
runTest('TEST043: matching_semantics_request_has_wildcard', test043_matchingSemanticsRequestHasWildcard);
|
|
2670
|
+
runTest('TEST044: matching_semantics_cap_has_wildcard', test044_matchingSemanticsCapHasWildcard);
|
|
2671
|
+
runTest('TEST045: matching_semantics_value_mismatch', test045_matchingSemanticsValueMismatch);
|
|
2672
|
+
runTest('TEST046: matching_semantics_fallback_pattern', test046_matchingSemanticsFallbackPattern);
|
|
2673
|
+
runTest('TEST047: matching_semantics_thumbnail_void_input', test047_matchingSemanticsThumbnailVoidInput);
|
|
2674
|
+
runTest('TEST048: matching_semantics_wildcard_direction', test048_matchingSemanticsWildcardDirection);
|
|
2675
|
+
runTest('TEST049: matching_semantics_cross_dimension', test049_matchingSemanticsCrossDimension);
|
|
2676
|
+
runTest('TEST050: matching_semantics_direction_mismatch', test050_matchingSemanticsDirectionMismatch);
|
|
2677
|
+
runTest('TEST890: direction_semantic_matching', test890_directionSemanticMatching);
|
|
2678
|
+
runTest('TEST891: direction_semantic_specificity', test891_directionSemanticSpecificity);
|
|
2679
|
+
|
|
2680
|
+
// validation.rs: TEST053-TEST056
|
|
2681
|
+
console.log('\n--- validation.rs ---');
|
|
2682
|
+
console.log(' SKIP TEST053: N/A for JS (Rust-only validation infrastructure)');
|
|
2683
|
+
runTest('TEST054: xv5_inline_spec_redefinition_detected', test054_xv5InlineSpecRedefinitionDetected);
|
|
2684
|
+
runTest('TEST055: xv5_new_inline_spec_allowed', test055_xv5NewInlineSpecAllowed);
|
|
2685
|
+
runTest('TEST056: xv5_empty_media_specs_allowed', test056_xv5EmptyMediaSpecsAllowed);
|
|
2686
|
+
|
|
2687
|
+
// media_urn.rs: TEST060-TEST078
|
|
2688
|
+
console.log('\n--- media_urn.rs ---');
|
|
2689
|
+
runTest('TEST060: wrong_prefix_fails', test060_wrongPrefixFails);
|
|
2690
|
+
runTest('TEST061: is_binary', test061_isBinary);
|
|
2691
|
+
runTest('TEST062: is_record', test062_isRecord);
|
|
2692
|
+
runTest('TEST063: is_scalar', test063_isScalar);
|
|
2693
|
+
runTest('TEST064: is_list', test064_isList);
|
|
2694
|
+
runTest('TEST065: is_opaque', test065_isOpaque);
|
|
2695
|
+
runTest('TEST066: is_json', test066_isJson);
|
|
2696
|
+
runTest('TEST067: is_text', test067_isText);
|
|
2697
|
+
runTest('TEST068: is_void', test068_isVoid);
|
|
2698
|
+
console.log(' SKIP TEST069-070: N/A for JS (Rust binary_media_urn_for_ext/text_media_urn_for_ext)');
|
|
2699
|
+
runTest('TEST071: to_string_roundtrip', test071_toStringRoundtrip);
|
|
2700
|
+
runTest('TEST072: constants_parse', test072_constantsParse);
|
|
2701
|
+
console.log(' SKIP TEST073: N/A for JS (Rust extension helpers)');
|
|
2702
|
+
runTest('TEST074: media_urn_matching', test074_mediaUrnMatching);
|
|
2703
|
+
runTest('TEST075: accepts', test075_accepts);
|
|
2704
|
+
runTest('TEST076: specificity', test076_specificity);
|
|
2705
|
+
runTest('TEST077: serde_roundtrip (JSON.stringify)', test077_serdeRoundtrip);
|
|
2706
|
+
runTest('TEST078: debug_matching_behavior', test078_debugMatchingBehavior);
|
|
2707
|
+
|
|
2708
|
+
// media_spec.rs: TEST088-TEST110
|
|
2709
|
+
console.log('\n--- media_spec.rs ---');
|
|
2710
|
+
console.log(' SKIP TEST088-090: N/A for JS (async registry, Rust-only)');
|
|
2711
|
+
runTest('TEST091: resolve_custom_media_spec', test091_resolveCustomMediaSpec);
|
|
2712
|
+
runTest('TEST092: resolve_custom_with_schema', test092_resolveCustomWithSchema);
|
|
2713
|
+
runTest('TEST093: resolve_unresolvable_fails_hard', test093_resolveUnresolvableFailsHard);
|
|
2714
|
+
console.log(' SKIP TEST094: N/A for JS (no registry concept)');
|
|
2715
|
+
console.log(' SKIP TEST095-098: N/A for JS (Rust serde/validation)');
|
|
2716
|
+
runTest('TEST099: resolved_is_binary', test099_resolvedIsBinary);
|
|
2717
|
+
runTest('TEST100: resolved_is_record', test100_resolvedIsRecord);
|
|
2718
|
+
runTest('TEST101: resolved_is_scalar', test101_resolvedIsScalar);
|
|
2719
|
+
runTest('TEST102: resolved_is_list', test102_resolvedIsList);
|
|
2720
|
+
runTest('TEST103: resolved_is_json', test103_resolvedIsJson);
|
|
2721
|
+
runTest('TEST104: resolved_is_text', test104_resolvedIsText);
|
|
2722
|
+
runTest('TEST105: metadata_propagation', test105_metadataPropagation);
|
|
2723
|
+
runTest('TEST106: metadata_with_validation', test106_metadataWithValidation);
|
|
2724
|
+
runTest('TEST107: extensions_propagation', test107_extensionsPropagation);
|
|
2725
|
+
runTest('TEST108: extensions_serialization', test108_extensionsSerialization);
|
|
2726
|
+
runTest('TEST109: extensions_with_metadata_and_validation', test109_extensionsWithMetadataAndValidation);
|
|
2727
|
+
runTest('TEST110: multiple_extensions', test110_multipleExtensions);
|
|
2728
|
+
|
|
2729
|
+
// cap_matrix.rs: TEST117-TEST131
|
|
2730
|
+
console.log('\n--- cap_matrix.rs ---');
|
|
2731
|
+
runTest('TEST117: cap_block_more_specific_wins', test117_capBlockMoreSpecificWins);
|
|
2732
|
+
runTest('TEST118: cap_block_tie_goes_to_first', test118_capBlockTieGoesToFirst);
|
|
2733
|
+
runTest('TEST119: cap_block_polls_all', test119_capBlockPollsAll);
|
|
2734
|
+
runTest('TEST120: cap_block_no_match', test120_capBlockNoMatch);
|
|
2735
|
+
runTest('TEST121: cap_block_fallback_scenario', test121_capBlockFallbackScenario);
|
|
2736
|
+
runTest('TEST122: cap_block_can_method', test122_capBlockCanMethod);
|
|
2737
|
+
runTest('TEST123: cap_block_registry_management', test123_capBlockRegistryManagement);
|
|
2738
|
+
runTest('TEST124: cap_graph_basic_construction', test124_capGraphBasicConstruction);
|
|
2739
|
+
runTest('TEST125: cap_graph_outgoing_incoming', test125_capGraphOutgoingIncoming);
|
|
2740
|
+
runTest('TEST126: cap_graph_can_convert', test126_capGraphCanConvert);
|
|
2741
|
+
runTest('TEST127: cap_graph_find_path', test127_capGraphFindPath);
|
|
2742
|
+
runTest('TEST128: cap_graph_find_all_paths', test128_capGraphFindAllPaths);
|
|
2743
|
+
runTest('TEST129: cap_graph_direct_edges_sorted_by_specificity', test129_capGraphGetDirectEdges);
|
|
2744
|
+
runTest('TEST130: cap_graph_stats', test130_capGraphStats);
|
|
2745
|
+
runTest('TEST131: cap_block_graph_integration', test131_capGraphWithCapBlock);
|
|
2746
|
+
console.log(' SKIP TEST132-134: N/A (already covered by TEST129-131)');
|
|
2747
|
+
|
|
2748
|
+
// caller.rs: TEST156-TEST159
|
|
2749
|
+
console.log('\n--- caller.rs (StdinSource) ---');
|
|
2750
|
+
runTest('TEST156: stdin_source_from_data', test156_stdinSourceFromData);
|
|
2751
|
+
runTest('TEST157: stdin_source_from_file_reference', test157_stdinSourceFromFileReference);
|
|
2752
|
+
runTest('TEST158: stdin_source_empty_data', test158_stdinSourceWithEmptyData);
|
|
2753
|
+
runTest('TEST159: stdin_source_binary_content', test159_stdinSourceWithBinaryContent);
|
|
2754
|
+
|
|
2755
|
+
// caller.rs: TEST274-TEST283
|
|
2756
|
+
console.log('\n--- caller.rs (CapArgumentValue) ---');
|
|
2757
|
+
runTest('TEST274: cap_argument_value_new', test274_capArgumentValueNew);
|
|
2758
|
+
runTest('TEST275: cap_argument_value_from_str', test275_capArgumentValueFromStr);
|
|
2759
|
+
runTest('TEST276: cap_argument_value_as_str_valid', test276_capArgumentValueAsStrValid);
|
|
2760
|
+
runTest('TEST277: cap_argument_value_as_str_invalid_utf8', test277_capArgumentValueAsStrInvalidUtf8);
|
|
2761
|
+
runTest('TEST278: cap_argument_value_empty', test278_capArgumentValueEmpty);
|
|
2762
|
+
console.log(' SKIP TEST279-281: N/A for JS (Rust Debug/Clone/Send traits)');
|
|
2763
|
+
runTest('TEST282: cap_argument_value_unicode', test282_capArgumentValueUnicode);
|
|
2764
|
+
runTest('TEST283: cap_argument_value_large_binary', test283_capArgumentValueLargeBinary);
|
|
2765
|
+
|
|
2766
|
+
// standard/caps.rs: TEST304-TEST312
|
|
2767
|
+
console.log('\n--- standard/caps.rs ---');
|
|
2768
|
+
runTest('TEST304: media_availability_output_constant', test304_mediaAvailabilityOutputConstant);
|
|
2769
|
+
runTest('TEST305: media_path_output_constant', test305_mediaPathOutputConstant);
|
|
2770
|
+
runTest('TEST306: availability_and_path_output_distinct', test306_availabilityAndPathOutputDistinct);
|
|
2771
|
+
runTest('TEST307: model_availability_urn', test307_modelAvailabilityUrn);
|
|
2772
|
+
runTest('TEST308: model_path_urn', test308_modelPathUrn);
|
|
2773
|
+
runTest('TEST309: model_availability_and_path_are_distinct', test309_modelAvailabilityAndPathAreDistinct);
|
|
2774
|
+
runTest('TEST310: llm_conversation_urn_unconstrained', test310_llmConversationUrnUnconstrained);
|
|
2775
|
+
runTest('TEST311: llm_conversation_urn_specs', test311_llmConversationUrnSpecs);
|
|
2776
|
+
runTest('TEST312: all_urn_builders_produce_valid_urns', test312_allUrnBuildersProduceValidUrns);
|
|
2777
|
+
|
|
2778
|
+
// JS-specific tests (no Rust number)
|
|
2779
|
+
console.log('\n--- JS-specific ---');
|
|
2780
|
+
runTest('JS: build_extension_index', testJS_buildExtensionIndex);
|
|
2781
|
+
runTest('JS: media_urns_for_extension', testJS_mediaUrnsForExtension);
|
|
2782
|
+
runTest('JS: get_extension_mappings', testJS_getExtensionMappings);
|
|
2783
|
+
runTest('JS: cap_with_media_specs', testJS_capWithMediaSpecs);
|
|
2784
|
+
runTest('JS: cap_json_serialization', testJS_capJSONSerialization);
|
|
2785
|
+
runTest('JS: stdin_source_kind_constants', testJS_stdinSourceKindConstants);
|
|
2786
|
+
runTest('JS: stdin_source_null_data', testJS_stdinSourceNullData);
|
|
2787
|
+
const p1 = runTest('JS: args_passed_to_executeCap', testJS_argsPassedToExecuteCap);
|
|
2788
|
+
if (p1) await p1;
|
|
2789
|
+
const p2 = runTest('JS: binary_arg_passed_to_executeCap', testJS_binaryArgPassedToExecuteCap);
|
|
2790
|
+
if (p2) await p2;
|
|
2791
|
+
runTest('JS: media_spec_construction', testJS_mediaSpecConstruction);
|
|
2792
|
+
|
|
2793
|
+
// plugin_repo: PluginRepoServer and PluginRepoClient tests
|
|
2794
|
+
console.log('\n--- plugin_repo ---');
|
|
2795
|
+
runTest('TEST320: plugin_info_construction', test320_pluginInfoConstruction);
|
|
2796
|
+
runTest('TEST321: plugin_info_is_signed', test321_pluginInfoIsSigned);
|
|
2797
|
+
runTest('TEST322: plugin_info_has_binary', test322_pluginInfoHasBinary);
|
|
2798
|
+
runTest('TEST323: plugin_repo_server_validate_registry', test323_pluginRepoServerValidateRegistry);
|
|
2799
|
+
runTest('TEST324: plugin_repo_server_transform_to_array', test324_pluginRepoServerTransformToArray);
|
|
2800
|
+
runTest('TEST325: plugin_repo_server_get_plugins', test325_pluginRepoServerGetPlugins);
|
|
2801
|
+
runTest('TEST326: plugin_repo_server_get_plugin_by_id', test326_pluginRepoServerGetPluginById);
|
|
2802
|
+
runTest('TEST327: plugin_repo_server_search_plugins', test327_pluginRepoServerSearchPlugins);
|
|
2803
|
+
runTest('TEST328: plugin_repo_server_get_by_category', test328_pluginRepoServerGetByCategory);
|
|
2804
|
+
runTest('TEST329: plugin_repo_server_get_by_cap', test329_pluginRepoServerGetByCap);
|
|
2805
|
+
runTest('TEST330: plugin_repo_client_update_cache', test330_pluginRepoClientUpdateCache);
|
|
2806
|
+
runTest('TEST331: plugin_repo_client_get_suggestions', test331_pluginRepoClientGetSuggestions);
|
|
2807
|
+
runTest('TEST332: plugin_repo_client_get_plugin', test332_pluginRepoClientGetPlugin);
|
|
2808
|
+
runTest('TEST333: plugin_repo_client_get_all_caps', test333_pluginRepoClientGetAllCaps);
|
|
2809
|
+
runTest('TEST334: plugin_repo_client_needs_sync', test334_pluginRepoClientNeedsSync);
|
|
2810
|
+
runTest('TEST335: plugin_repo_server_client_integration', test335_pluginRepoServerClientIntegration);
|
|
2811
|
+
|
|
2812
|
+
// media_urn.rs: TEST546-TEST558 (MediaUrn predicates)
|
|
2813
|
+
console.log('\n--- media_urn.rs (predicates) ---');
|
|
2814
|
+
runTest('TEST546: is_image', test546_isImage);
|
|
2815
|
+
runTest('TEST547: is_audio', test547_isAudio);
|
|
2816
|
+
runTest('TEST548: is_video', test548_isVideo);
|
|
2817
|
+
runTest('TEST549: is_numeric', test549_isNumeric);
|
|
2818
|
+
runTest('TEST550: is_bool', test550_isBool);
|
|
2819
|
+
runTest('TEST551: is_file_path', test551_isFilePath);
|
|
2820
|
+
runTest('TEST552: is_file_path_array', test552_isFilePathArray);
|
|
2821
|
+
runTest('TEST553: is_any_file_path', test553_isAnyFilePath);
|
|
2822
|
+
console.log(' SKIP TEST554: N/A for JS (collection types removed from capdag)');
|
|
2823
|
+
console.log(' SKIP TEST555: N/A for JS (with_tag/without_tag on MediaUrn)');
|
|
2824
|
+
console.log(' SKIP TEST556: N/A for JS (image_media_urn_for_ext helper)');
|
|
2825
|
+
console.log(' SKIP TEST557: N/A for JS (audio_media_urn_for_ext helper)');
|
|
2826
|
+
runTest('TEST558: predicate_constant_consistency', test558_predicateConstantConsistency);
|
|
2827
|
+
|
|
2828
|
+
// cap_urn.rs: TEST559-TEST567 (CapUrn tier tests)
|
|
2829
|
+
console.log('\n--- cap_urn.rs (tier tests) ---');
|
|
2830
|
+
runTest('TEST559: without_tag', test559_withoutTag);
|
|
2831
|
+
runTest('TEST560: with_in_out_spec', test560_withInOutSpec);
|
|
2832
|
+
console.log(' SKIP TEST561: N/A for JS (in_media_urn/out_media_urn)');
|
|
2833
|
+
console.log(' SKIP TEST562: N/A for JS (canonical_option)');
|
|
2834
|
+
runTest('TEST563: find_all_matches', test563_findAllMatches);
|
|
2835
|
+
runTest('TEST564: are_compatible', test564_areCompatible);
|
|
2836
|
+
console.log(' SKIP TEST565: N/A for JS (tags_to_string)');
|
|
2837
|
+
runTest('TEST566: with_tag_ignores_in_out', test566_withTagIgnoresInOut);
|
|
2838
|
+
console.log(' SKIP TEST567: N/A for JS (conforms_to_str/accepts_str)');
|
|
2839
|
+
|
|
2840
|
+
// cap_urn.rs: TEST639-TEST653 (Cap URN wildcard tests)
|
|
2841
|
+
console.log('\n--- cap_urn.rs (wildcard tests) ---');
|
|
2842
|
+
console.log(' SKIP TEST639-642: N/A for JS (implicit wildcard defaults)');
|
|
2843
|
+
runTest('TEST643: explicit_asterisk_is_wildcard', test643_explicitAsteriskIsWildcard);
|
|
2844
|
+
runTest('TEST644: specific_in_wildcard_out', test644_specificInWildcardOut);
|
|
2845
|
+
runTest('TEST645: wildcard_in_specific_out', test645_wildcardInSpecificOut);
|
|
2846
|
+
console.log(' SKIP TEST646-647: N/A for JS (invalid spec validation differs)');
|
|
2847
|
+
runTest('TEST648: wildcard_accepts_specific', test648_wildcardAcceptsSpecific);
|
|
2848
|
+
runTest('TEST649: specificity_scoring', test649_specificityScoring);
|
|
2849
|
+
console.log(' SKIP TEST650: N/A for JS (requires in/out)');
|
|
2850
|
+
runTest('TEST651: identity_forms_equivalent', test651_identityFormsEquivalent);
|
|
2851
|
+
console.log(' SKIP TEST652: N/A for JS (CAP_IDENTITY constant)');
|
|
2852
|
+
runTest('TEST653: identity_routing_isolation', test653_identityRoutingIsolation);
|
|
2853
|
+
|
|
2854
|
+
// Summary
|
|
2855
|
+
console.log(`\n${passCount + failCount} tests: ${passCount} passed, ${failCount} failed`);
|
|
2856
|
+
if (failCount > 0) {
|
|
2857
|
+
console.log('ERR Some tests failed!');
|
|
2858
|
+
process.exit(1);
|
|
2859
|
+
} else {
|
|
2860
|
+
console.log('OK All tests passed!');
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// Run the tests
|
|
2865
|
+
if (require.main === module) {
|
|
2866
|
+
runTests()
|
|
2867
|
+
.then(() => process.exit(0))
|
|
2868
|
+
.catch(error => {
|
|
2869
|
+
console.error('\nERR Test failed:', error.message);
|
|
2870
|
+
process.exit(1);
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
module.exports = { runTests };
|