@unrdf/kgc-runtime 26.4.2
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/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admission Gate & Receipt Chain Tests
|
|
3
|
+
* Comprehensive test suite covering all admission gate features
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
7
|
+
import { AdmissionGate } from '../src/admission-gate.mjs';
|
|
8
|
+
|
|
9
|
+
describe('AdmissionGate', () => {
|
|
10
|
+
let gate;
|
|
11
|
+
let mockTime = 1000000n;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Create gate with mock time source for deterministic tests
|
|
15
|
+
gate = new AdmissionGate({
|
|
16
|
+
timeSource: () => mockTime++,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('Basic Admit/Deny', () => {
|
|
21
|
+
it('should admit valid operation within bounds', async () => {
|
|
22
|
+
const delta = {
|
|
23
|
+
operation: 'add',
|
|
24
|
+
triples: [{ s: 'subject', p: 'predicate', o: 'object' }],
|
|
25
|
+
};
|
|
26
|
+
const bounds = { maxTriples: 10 };
|
|
27
|
+
|
|
28
|
+
const receipt = await gate.admit(delta, bounds, []);
|
|
29
|
+
|
|
30
|
+
expect(receipt.result).toBe('admit');
|
|
31
|
+
expect(receipt.reason).toBe('All checks passed');
|
|
32
|
+
expect(receipt.operation).toBe('add');
|
|
33
|
+
expect(receipt.timestamp_ns).toBe(1000000n);
|
|
34
|
+
expect(receipt.hash).toBeTruthy();
|
|
35
|
+
expect(receipt.hash).toMatch(/^[a-f0-9]{64}$/); // BLAKE3 produces 64 hex chars
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should deny operation exceeding triple bounds', async () => {
|
|
39
|
+
const delta = {
|
|
40
|
+
operation: 'add',
|
|
41
|
+
triples: [
|
|
42
|
+
{ s: 's1', p: 'p1', o: 'o1' },
|
|
43
|
+
{ s: 's2', p: 'p2', o: 'o2' },
|
|
44
|
+
{ s: 's3', p: 'p3', o: 'o3' },
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
const bounds = { maxTriples: 2 };
|
|
48
|
+
|
|
49
|
+
const receipt = await gate.admit(delta, bounds, []);
|
|
50
|
+
|
|
51
|
+
expect(receipt.result).toBe('deny');
|
|
52
|
+
expect(receipt.reason).toContain('Triple count 3 exceeds bound 2');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should deny operation exceeding query bounds', async () => {
|
|
56
|
+
const bounds = { maxQueries: 2 };
|
|
57
|
+
|
|
58
|
+
// First query - admit
|
|
59
|
+
const receipt1 = await gate.admit({ operation: 'query', query: 'SELECT * WHERE { ?s ?p ?o }' }, bounds, []);
|
|
60
|
+
expect(receipt1.result).toBe('admit');
|
|
61
|
+
|
|
62
|
+
// Second query - admit
|
|
63
|
+
const receipt2 = await gate.admit({ operation: 'query', query: 'SELECT * WHERE { ?s ?p ?o }' }, bounds, []);
|
|
64
|
+
expect(receipt2.result).toBe('admit');
|
|
65
|
+
|
|
66
|
+
// Third query - deny (exceeds maxQueries)
|
|
67
|
+
const receipt3 = await gate.admit({ operation: 'query', query: 'SELECT * WHERE { ?s ?p ?o }' }, bounds, []);
|
|
68
|
+
expect(receipt3.result).toBe('deny');
|
|
69
|
+
expect(receipt3.reason).toContain('exceeds bound 2');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should validate predicate checks', async () => {
|
|
73
|
+
const delta = { operation: 'add', triples: [] };
|
|
74
|
+
const predicates = [
|
|
75
|
+
{
|
|
76
|
+
name: 'hasTriples',
|
|
77
|
+
check: (d) => d.triples && d.triples.length > 0,
|
|
78
|
+
description: 'must have at least one triple',
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const receipt = await gate.admit(delta, {}, predicates);
|
|
83
|
+
|
|
84
|
+
expect(receipt.result).toBe('deny');
|
|
85
|
+
expect(receipt.reason).toContain("Predicate 'hasTriples' failed");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle predicate errors gracefully', async () => {
|
|
89
|
+
const delta = { operation: 'add', triples: [] };
|
|
90
|
+
const predicates = [
|
|
91
|
+
{
|
|
92
|
+
name: 'throwsError',
|
|
93
|
+
check: () => {
|
|
94
|
+
throw new Error('Intentional test error');
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const receipt = await gate.admit(delta, {}, predicates);
|
|
100
|
+
|
|
101
|
+
expect(receipt.result).toBe('deny');
|
|
102
|
+
expect(receipt.reason).toContain('threw error');
|
|
103
|
+
expect(receipt.reason).toContain('Intentional test error');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('Receipt Chaining', () => {
|
|
108
|
+
it('should create first receipt with null parent', async () => {
|
|
109
|
+
// Create fresh gate for this test
|
|
110
|
+
const freshGate = new AdmissionGate({
|
|
111
|
+
timeSource: () => 2000000n,
|
|
112
|
+
});
|
|
113
|
+
const delta = { operation: 'add', triples: [] };
|
|
114
|
+
const receipt = await freshGate.admit(delta, {}, []);
|
|
115
|
+
|
|
116
|
+
expect(receipt.parent_receipt_id).toBeNull();
|
|
117
|
+
expect(receipt.id).toBeTruthy();
|
|
118
|
+
expect(receipt.timestamp_ns).toBe(2000000n);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should chain receipts with parent references', async () => {
|
|
122
|
+
const delta1 = { operation: 'add', triples: [] };
|
|
123
|
+
const delta2 = { operation: 'query', query: 'SELECT *' };
|
|
124
|
+
|
|
125
|
+
const receipt1 = await gate.admit(delta1, {}, []);
|
|
126
|
+
const receipt2 = await gate.admit(delta2, {}, []);
|
|
127
|
+
|
|
128
|
+
expect(receipt1.parent_receipt_id).toBeNull();
|
|
129
|
+
expect(receipt2.parent_receipt_id).toBe(receipt1.id);
|
|
130
|
+
expect(receipt2.timestamp_ns).toBeGreaterThan(receipt1.timestamp_ns);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should maintain monotonic timestamps in chain', async () => {
|
|
134
|
+
const deltas = [{ operation: 'add', triples: [] }, { operation: 'add', triples: [] }, { operation: 'add', triples: [] }];
|
|
135
|
+
|
|
136
|
+
const receipts = [];
|
|
137
|
+
for (const delta of deltas) {
|
|
138
|
+
receipts.push(await gate.admit(delta, {}, []));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Verify monotonic increase
|
|
142
|
+
for (let i = 1; i < receipts.length; i++) {
|
|
143
|
+
expect(receipts[i].timestamp_ns).toBeGreaterThan(receipts[i - 1].timestamp_ns);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should build valid receipt chain', async () => {
|
|
148
|
+
// Create multiple operations
|
|
149
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
150
|
+
await gate.admit({ operation: 'query', query: 'SELECT *' }, {}, []);
|
|
151
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
152
|
+
|
|
153
|
+
const chain = gate.getReceiptChain();
|
|
154
|
+
|
|
155
|
+
expect(chain).toHaveLength(3);
|
|
156
|
+
expect(chain[0].parent_receipt_id).toBeNull();
|
|
157
|
+
expect(chain[1].parent_receipt_id).toBe(chain[0].id);
|
|
158
|
+
expect(chain[2].parent_receipt_id).toBe(chain[1].id);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('Bounds Violation Detection', () => {
|
|
163
|
+
it('should detect triple count violations', async () => {
|
|
164
|
+
const delta = {
|
|
165
|
+
operation: 'add',
|
|
166
|
+
triples: [
|
|
167
|
+
{ s: 's1', p: 'p1', o: 'o1' },
|
|
168
|
+
{ s: 's2', p: 'p2', o: 'o2' },
|
|
169
|
+
{ s: 's3', p: 'p3', o: 'o3' },
|
|
170
|
+
{ s: 's4', p: 'p4', o: 'o4' },
|
|
171
|
+
{ s: 's5', p: 'p5', o: 'o5' },
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
const bounds = { maxTriples: 3 };
|
|
175
|
+
|
|
176
|
+
const receipt = await gate.admit(delta, bounds, []);
|
|
177
|
+
|
|
178
|
+
expect(receipt.result).toBe('deny');
|
|
179
|
+
expect(receipt.reason).toBe('Triple count 5 exceeds bound 3');
|
|
180
|
+
expect(receipt.bounds_used.maxTriples).toBe(3);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should track bounds in receipt', async () => {
|
|
184
|
+
const delta = { operation: 'add', triples: [] };
|
|
185
|
+
const bounds = { maxTriples: 100, maxMemoryMB: 512, maxTimeMs: 1000 };
|
|
186
|
+
|
|
187
|
+
const receipt = await gate.admit(delta, bounds, []);
|
|
188
|
+
|
|
189
|
+
expect(receipt.bounds_used).toEqual(bounds);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should allow operation at exact bound', async () => {
|
|
193
|
+
const delta = {
|
|
194
|
+
operation: 'add',
|
|
195
|
+
triples: [
|
|
196
|
+
{ s: 's1', p: 'p1', o: 'o1' },
|
|
197
|
+
{ s: 's2', p: 'p2', o: 'o2' },
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
const bounds = { maxTriples: 2 };
|
|
201
|
+
|
|
202
|
+
const receipt = await gate.admit(delta, bounds, []);
|
|
203
|
+
|
|
204
|
+
expect(receipt.result).toBe('admit');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should work with no bounds specified', async () => {
|
|
208
|
+
const delta = { operation: 'add', triples: [{ s: 's', p: 'p', o: 'o' }] };
|
|
209
|
+
|
|
210
|
+
const receipt = await gate.admit(delta, {}, []);
|
|
211
|
+
|
|
212
|
+
expect(receipt.result).toBe('admit');
|
|
213
|
+
expect(receipt.bounds_used).toEqual({});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('Forbidden Operations Blocking', () => {
|
|
218
|
+
it('should block forbidden update operation without receipt', async () => {
|
|
219
|
+
const delta = { operation: 'update', triples: [] };
|
|
220
|
+
|
|
221
|
+
const receipt = await gate.admit(delta, {}, []);
|
|
222
|
+
|
|
223
|
+
expect(receipt.result).toBe('deny');
|
|
224
|
+
expect(receipt.reason).toContain("Forbidden operation 'update' without valid receipt");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should block forbidden delete operation without receipt', async () => {
|
|
228
|
+
const delta = { operation: 'delete', triples: [] };
|
|
229
|
+
|
|
230
|
+
const receipt = await gate.admit(delta, {}, []);
|
|
231
|
+
|
|
232
|
+
expect(receipt.result).toBe('deny');
|
|
233
|
+
expect(receipt.reason).toContain("Forbidden operation 'delete' without valid receipt");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should allow update operation with prior admitted update receipt', async () => {
|
|
237
|
+
// First, admit an update to establish authorization
|
|
238
|
+
const delta1 = { operation: 'update', triples: [] };
|
|
239
|
+
const receipt1 = await gate.admit(delta1, {}, []);
|
|
240
|
+
expect(receipt1.result).toBe('deny'); // First one denied
|
|
241
|
+
|
|
242
|
+
// Manually add an admitted update receipt to chain (simulating authorization)
|
|
243
|
+
gate.receiptChain.push({
|
|
244
|
+
id: 'auth_receipt',
|
|
245
|
+
timestamp_ns: mockTime++,
|
|
246
|
+
operation: 'update',
|
|
247
|
+
result: 'admit',
|
|
248
|
+
reason: 'Authorized',
|
|
249
|
+
bounds_used: {},
|
|
250
|
+
parent_receipt_id: receipt1.id,
|
|
251
|
+
hash: 'mock_hash',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Now update should be allowed
|
|
255
|
+
const delta2 = { operation: 'update', triples: [] };
|
|
256
|
+
const receipt2 = await gate.admit(delta2, {}, []);
|
|
257
|
+
expect(receipt2.result).toBe('admit');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should allow non-forbidden operations', async () => {
|
|
261
|
+
const addDelta = { operation: 'add', triples: [] };
|
|
262
|
+
const queryDelta = { operation: 'query', query: 'SELECT *' };
|
|
263
|
+
|
|
264
|
+
const receipt1 = await gate.admit(addDelta, {}, []);
|
|
265
|
+
const receipt2 = await gate.admit(queryDelta, {}, []);
|
|
266
|
+
|
|
267
|
+
expect(receipt1.result).toBe('admit');
|
|
268
|
+
expect(receipt2.result).toBe('admit');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should support custom forbidden operations', async () => {
|
|
272
|
+
const customGate = new AdmissionGate({
|
|
273
|
+
forbiddenOperations: ['query'],
|
|
274
|
+
timeSource: () => mockTime++,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const delta = { operation: 'query', query: 'SELECT *' };
|
|
278
|
+
const receipt = await customGate.admit(delta, {}, []);
|
|
279
|
+
|
|
280
|
+
expect(receipt.result).toBe('deny');
|
|
281
|
+
expect(receipt.reason).toContain("Forbidden operation 'query'");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('Chain Verification', () => {
|
|
286
|
+
it('should verify valid chain', async () => {
|
|
287
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
288
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
289
|
+
await gate.admit({ operation: 'query', query: 'SELECT *' }, {}, []);
|
|
290
|
+
|
|
291
|
+
const chain = gate.getReceiptChain();
|
|
292
|
+
const isValid = await gate.verifyChain(chain);
|
|
293
|
+
|
|
294
|
+
expect(isValid).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should detect tampered receipt hash', async () => {
|
|
298
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
299
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
300
|
+
|
|
301
|
+
const chain = gate.getReceiptChain();
|
|
302
|
+
|
|
303
|
+
// Tamper with hash
|
|
304
|
+
chain[1].hash = 'tampered_hash_0000000000000000000000000000000000000000000000000000000000';
|
|
305
|
+
|
|
306
|
+
const isValid = await gate.verifyChain(chain);
|
|
307
|
+
|
|
308
|
+
expect(isValid).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should detect broken chain linkage', async () => {
|
|
312
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
313
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
314
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
315
|
+
|
|
316
|
+
const chain = gate.getReceiptChain();
|
|
317
|
+
|
|
318
|
+
// Break linkage
|
|
319
|
+
chain[2].parent_receipt_id = 'wrong_parent_id';
|
|
320
|
+
|
|
321
|
+
const isValid = await gate.verifyChain(chain);
|
|
322
|
+
|
|
323
|
+
expect(isValid).toBe(false);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should detect non-monotonic timestamps', async () => {
|
|
327
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
328
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
329
|
+
|
|
330
|
+
const chain = gate.getReceiptChain();
|
|
331
|
+
|
|
332
|
+
// Make timestamp go backwards
|
|
333
|
+
chain[1].timestamp_ns = chain[0].timestamp_ns - 1n;
|
|
334
|
+
|
|
335
|
+
const isValid = await gate.verifyChain(chain);
|
|
336
|
+
|
|
337
|
+
expect(isValid).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should reject empty chain', async () => {
|
|
341
|
+
const isValid = await gate.verifyChain([]);
|
|
342
|
+
|
|
343
|
+
expect(isValid).toBe(false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should verify single receipt chain', async () => {
|
|
347
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
348
|
+
|
|
349
|
+
const chain = gate.getReceiptChain();
|
|
350
|
+
const isValid = await gate.verifyChain(chain);
|
|
351
|
+
|
|
352
|
+
expect(isValid).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('Merkle Root Computation', () => {
|
|
357
|
+
it('should compute Merkle root for single receipt', async () => {
|
|
358
|
+
const delta = { operation: 'add', triples: [] };
|
|
359
|
+
const receipt = await gate.admit(delta, {}, []);
|
|
360
|
+
|
|
361
|
+
const root = await gate.computeMerkleRoot([receipt]);
|
|
362
|
+
|
|
363
|
+
expect(root).toBe(receipt.hash);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should compute Merkle root for multiple receipts', async () => {
|
|
367
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
368
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
369
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
370
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
371
|
+
|
|
372
|
+
const chain = gate.getReceiptChain();
|
|
373
|
+
const root = await gate.computeMerkleRoot(chain);
|
|
374
|
+
|
|
375
|
+
expect(root).toBeTruthy();
|
|
376
|
+
expect(root).toMatch(/^[a-f0-9]{64}$/); // BLAKE3 hash
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should compute deterministic Merkle root', async () => {
|
|
380
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
381
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
382
|
+
|
|
383
|
+
const chain = gate.getReceiptChain();
|
|
384
|
+
const root1 = await gate.computeMerkleRoot(chain);
|
|
385
|
+
const root2 = await gate.computeMerkleRoot(chain);
|
|
386
|
+
|
|
387
|
+
expect(root1).toBe(root2);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should handle odd number of receipts', async () => {
|
|
391
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
392
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
393
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
394
|
+
|
|
395
|
+
const chain = gate.getReceiptChain();
|
|
396
|
+
const root = await gate.computeMerkleRoot(chain);
|
|
397
|
+
|
|
398
|
+
expect(root).toBeTruthy();
|
|
399
|
+
expect(root).toMatch(/^[a-f0-9]{64}$/);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should handle empty receipt array', async () => {
|
|
403
|
+
const root = await gate.computeMerkleRoot([]);
|
|
404
|
+
|
|
405
|
+
expect(root).toBeTruthy();
|
|
406
|
+
expect(root).toMatch(/^[a-f0-9]{64}$/);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe('Batch Anchoring', () => {
|
|
411
|
+
it('should batch anchor multiple operations', async () => {
|
|
412
|
+
const deltas = [
|
|
413
|
+
{ operation: 'add', triples: [] },
|
|
414
|
+
{ operation: 'add', triples: [] },
|
|
415
|
+
{ operation: 'query', query: 'SELECT *' },
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
const batchReceipt = await gate.batchAnchor(deltas, { maxTriples: 100 });
|
|
419
|
+
|
|
420
|
+
expect(batchReceipt.type).toBe('batch');
|
|
421
|
+
expect(batchReceipt.operation_count).toBe(3);
|
|
422
|
+
expect(batchReceipt.merkle_root).toBeTruthy();
|
|
423
|
+
expect(batchReceipt.receipts).toHaveLength(3);
|
|
424
|
+
expect(batchReceipt.all_admitted).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should sort operations deterministically', async () => {
|
|
428
|
+
// Create two fresh gates with same time source for deterministic comparison
|
|
429
|
+
let time1 = 3000000n;
|
|
430
|
+
const gate1 = new AdmissionGate({
|
|
431
|
+
timeSource: () => time1++,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
let time2 = 3000000n;
|
|
435
|
+
const gate2 = new AdmissionGate({
|
|
436
|
+
timeSource: () => time2++,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const deltas1 = [
|
|
440
|
+
{ operation: 'query', query: 'SELECT *' },
|
|
441
|
+
{ operation: 'add', triples: [] },
|
|
442
|
+
{ operation: 'add', triples: [] },
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
const batch1 = await gate1.batchAnchor(deltas1, {});
|
|
446
|
+
|
|
447
|
+
const deltas2 = [
|
|
448
|
+
{ operation: 'add', triples: [] },
|
|
449
|
+
{ operation: 'query', query: 'SELECT *' },
|
|
450
|
+
{ operation: 'add', triples: [] },
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
const batch2 = await gate2.batchAnchor(deltas2, {});
|
|
454
|
+
|
|
455
|
+
// Merkle roots should be the same (deterministic sorting)
|
|
456
|
+
expect(batch1.merkle_root).toBe(batch2.merkle_root);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should detect failures in batch', async () => {
|
|
460
|
+
const deltas = [
|
|
461
|
+
{ operation: 'add', triples: [] },
|
|
462
|
+
{ operation: 'update', triples: [] }, // Forbidden - will be denied
|
|
463
|
+
{ operation: 'add', triples: [] },
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
const batchReceipt = await gate.batchAnchor(deltas, {});
|
|
467
|
+
|
|
468
|
+
expect(batchReceipt.all_admitted).toBe(false);
|
|
469
|
+
// After sorting, operations are: add, add, update (alphabetical)
|
|
470
|
+
// So update is at index 2
|
|
471
|
+
expect(batchReceipt.receipts[2].result).toBe('deny');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should throw on empty batch', async () => {
|
|
475
|
+
await expect(gate.batchAnchor([], {})).rejects.toThrow('non-empty array');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should include bounds in batch receipts', async () => {
|
|
479
|
+
const deltas = [
|
|
480
|
+
{ operation: 'add', triples: [] },
|
|
481
|
+
{ operation: 'add', triples: [] },
|
|
482
|
+
];
|
|
483
|
+
const bounds = { maxTriples: 50, maxTimeMs: 1000 };
|
|
484
|
+
|
|
485
|
+
const batchReceipt = await gate.batchAnchor(deltas, bounds);
|
|
486
|
+
|
|
487
|
+
expect(batchReceipt.receipts[0].bounds_used).toEqual(bounds);
|
|
488
|
+
expect(batchReceipt.receipts[1].bounds_used).toEqual(bounds);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe('Preserve(Q) Invariant', () => {
|
|
493
|
+
it('should preserve query results on first execution', async () => {
|
|
494
|
+
const delta = { operation: 'query', query: 'SELECT * WHERE { ?s ?p ?o }' };
|
|
495
|
+
|
|
496
|
+
const receipt = await gate.admit(delta, {}, []);
|
|
497
|
+
|
|
498
|
+
expect(receipt.result).toBe('admit');
|
|
499
|
+
expect(gate.preservedQueries.has('SELECT * WHERE { ?s ?p ?o }')).toBe(true);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('should track query execution count', async () => {
|
|
503
|
+
const query = 'SELECT * WHERE { ?s ?p ?o }';
|
|
504
|
+
|
|
505
|
+
await gate.admit({ operation: 'query', query }, {}, []);
|
|
506
|
+
await gate.admit({ operation: 'query', query }, {}, []);
|
|
507
|
+
await gate.admit({ operation: 'query', query }, {}, []);
|
|
508
|
+
|
|
509
|
+
const preserved = gate.preservedQueries.get(query);
|
|
510
|
+
expect(preserved.count).toBe(3);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should preserve different queries independently', async () => {
|
|
514
|
+
await gate.admit({ operation: 'query', query: 'SELECT * WHERE { ?s ?p ?o }' }, {}, []);
|
|
515
|
+
await gate.admit({ operation: 'query', query: 'SELECT ?s WHERE { ?s a ?type }' }, {}, []);
|
|
516
|
+
|
|
517
|
+
expect(gate.preservedQueries.size).toBe(2);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
describe('Edge Cases', () => {
|
|
522
|
+
it('should handle metadata in delta', async () => {
|
|
523
|
+
const delta = {
|
|
524
|
+
operation: 'add',
|
|
525
|
+
triples: [],
|
|
526
|
+
metadata: { user: 'test', timestamp: '2025-01-01' },
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const receipt = await gate.admit(delta, {}, []);
|
|
530
|
+
|
|
531
|
+
expect(receipt.result).toBe('admit');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('should validate delta schema', async () => {
|
|
535
|
+
const invalidDelta = {
|
|
536
|
+
operation: 'invalid_operation',
|
|
537
|
+
triples: [],
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
await expect(gate.admit(invalidDelta, {}, [])).rejects.toThrow();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should validate bounds schema', async () => {
|
|
544
|
+
const delta = { operation: 'add', triples: [] };
|
|
545
|
+
const invalidBounds = { maxTriples: -5 }; // Negative not allowed
|
|
546
|
+
|
|
547
|
+
await expect(gate.admit(delta, invalidBounds, [])).rejects.toThrow();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should clear receipt chain', async () => {
|
|
551
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
552
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
553
|
+
|
|
554
|
+
expect(gate.getReceiptChain()).toHaveLength(2);
|
|
555
|
+
|
|
556
|
+
gate.clearReceiptChain();
|
|
557
|
+
|
|
558
|
+
expect(gate.getReceiptChain()).toHaveLength(0);
|
|
559
|
+
expect(gate.preservedQueries.size).toBe(0);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should handle operation without triples', async () => {
|
|
563
|
+
const delta = { operation: 'query', query: 'SELECT *' };
|
|
564
|
+
|
|
565
|
+
const receipt = await gate.admit(delta, {}, []);
|
|
566
|
+
|
|
567
|
+
expect(receipt.result).toBe('admit');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should generate unique receipt IDs', async () => {
|
|
571
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
572
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
573
|
+
await gate.admit({ operation: 'add', triples: [] }, {}, []);
|
|
574
|
+
|
|
575
|
+
const chain = gate.getReceiptChain();
|
|
576
|
+
const ids = chain.map((r) => r.id);
|
|
577
|
+
|
|
578
|
+
// All IDs should be unique
|
|
579
|
+
const uniqueIds = new Set(ids);
|
|
580
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Version Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
APIVersionManager,
|
|
8
|
+
CURRENT_API_VERSION,
|
|
9
|
+
isPluginCompatible,
|
|
10
|
+
validatePluginVersion,
|
|
11
|
+
} from '../src/api-version.mjs';
|
|
12
|
+
|
|
13
|
+
describe('APIVersionManager', () => {
|
|
14
|
+
let versionManager;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
versionManager = new APIVersionManager();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return current API version', () => {
|
|
21
|
+
expect(versionManager.getCurrentVersion()).toBe(CURRENT_API_VERSION);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should check version compatibility', () => {
|
|
25
|
+
// Same major.minor = compatible
|
|
26
|
+
expect(versionManager.isCompatible('5.0.0', '5.0.1')).toBe(true);
|
|
27
|
+
|
|
28
|
+
// Higher minor = compatible
|
|
29
|
+
expect(versionManager.isCompatible('5.0.0', '5.1.0')).toBe(true);
|
|
30
|
+
|
|
31
|
+
// Lower minor = not compatible
|
|
32
|
+
expect(versionManager.isCompatible('5.1.0', '5.0.0')).toBe(false);
|
|
33
|
+
|
|
34
|
+
// Different major = not compatible
|
|
35
|
+
expect(versionManager.isCompatible('5.0.0', '6.0.0')).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should detect deprecated versions', () => {
|
|
39
|
+
const deprecated = versionManager.isDeprecated('4.0.0');
|
|
40
|
+
expect(deprecated).toBe(true);
|
|
41
|
+
|
|
42
|
+
const stable = versionManager.isDeprecated('5.0.1');
|
|
43
|
+
expect(stable).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should calculate days until removal for deprecated versions', () => {
|
|
47
|
+
const days = versionManager.getDaysUntilRemoval('4.0.0');
|
|
48
|
+
expect(typeof days).toBe('number');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should list all versions', () => {
|
|
52
|
+
const versions = versionManager.listVersions();
|
|
53
|
+
expect(versions.length).toBeGreaterThan(0);
|
|
54
|
+
expect(versions.some(v => v.version === CURRENT_API_VERSION)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Plugin Version Validation', () => {
|
|
59
|
+
it('should validate compatible plugin versions', () => {
|
|
60
|
+
expect(isPluginCompatible('5.0.0')).toBe(true);
|
|
61
|
+
expect(isPluginCompatible('5.0.1')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should reject incompatible plugin versions', () => {
|
|
65
|
+
expect(isPluginCompatible('6.0.0')).toBe(false);
|
|
66
|
+
expect(isPluginCompatible('4.0.0')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should throw for removed versions', () => {
|
|
70
|
+
expect(() => {
|
|
71
|
+
validatePluginVersion('3.0.0');
|
|
72
|
+
}).toThrow(/has been removed/);
|
|
73
|
+
});
|
|
74
|
+
});
|