@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.
Files changed (70) hide show
  1. package/IMPLEMENTATION_SUMMARY.json +150 -0
  2. package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
  3. package/README.md +98 -0
  4. package/TRANSACTION_IMPLEMENTATION.json +119 -0
  5. package/capability-map.md +93 -0
  6. package/docs/api-stability.md +269 -0
  7. package/docs/extensions/plugin-development.md +382 -0
  8. package/package.json +40 -0
  9. package/plugins/registry.json +35 -0
  10. package/src/admission-gate.mjs +414 -0
  11. package/src/api-version.mjs +373 -0
  12. package/src/atomic-admission.mjs +310 -0
  13. package/src/bounds.mjs +289 -0
  14. package/src/bulkhead-manager.mjs +280 -0
  15. package/src/capsule.mjs +524 -0
  16. package/src/crdt.mjs +361 -0
  17. package/src/enhanced-bounds.mjs +614 -0
  18. package/src/executor.mjs +73 -0
  19. package/src/freeze-restore.mjs +521 -0
  20. package/src/index.mjs +62 -0
  21. package/src/materialized-views.mjs +371 -0
  22. package/src/merge.mjs +472 -0
  23. package/src/plugin-isolation.mjs +392 -0
  24. package/src/plugin-manager.mjs +441 -0
  25. package/src/projections-api.mjs +336 -0
  26. package/src/projections-cli.mjs +238 -0
  27. package/src/projections-docs.mjs +300 -0
  28. package/src/projections-ide.mjs +278 -0
  29. package/src/receipt.mjs +340 -0
  30. package/src/rollback.mjs +258 -0
  31. package/src/saga-orchestrator.mjs +355 -0
  32. package/src/schemas.mjs +1330 -0
  33. package/src/storage-optimization.mjs +359 -0
  34. package/src/tool-registry.mjs +272 -0
  35. package/src/transaction.mjs +466 -0
  36. package/src/validators.mjs +485 -0
  37. package/src/work-item.mjs +449 -0
  38. package/templates/plugin-template/README.md +58 -0
  39. package/templates/plugin-template/index.mjs +162 -0
  40. package/templates/plugin-template/plugin.json +19 -0
  41. package/test/admission-gate.test.mjs +583 -0
  42. package/test/api-version.test.mjs +74 -0
  43. package/test/atomic-admission.test.mjs +155 -0
  44. package/test/bounds.test.mjs +341 -0
  45. package/test/bulkhead-manager.test.mjs +236 -0
  46. package/test/capsule.test.mjs +625 -0
  47. package/test/crdt.test.mjs +215 -0
  48. package/test/enhanced-bounds.test.mjs +487 -0
  49. package/test/freeze-restore.test.mjs +472 -0
  50. package/test/materialized-views.test.mjs +243 -0
  51. package/test/merge.test.mjs +665 -0
  52. package/test/plugin-isolation.test.mjs +109 -0
  53. package/test/plugin-manager.test.mjs +208 -0
  54. package/test/projections-api.test.mjs +293 -0
  55. package/test/projections-cli.test.mjs +204 -0
  56. package/test/projections-docs.test.mjs +173 -0
  57. package/test/projections-ide.test.mjs +230 -0
  58. package/test/receipt.test.mjs +295 -0
  59. package/test/rollback.test.mjs +132 -0
  60. package/test/saga-orchestrator.test.mjs +279 -0
  61. package/test/schemas.test.mjs +716 -0
  62. package/test/storage-optimization.test.mjs +503 -0
  63. package/test/tool-registry.test.mjs +341 -0
  64. package/test/transaction.test.mjs +189 -0
  65. package/test/validators.test.mjs +463 -0
  66. package/test/work-item.test.mjs +548 -0
  67. package/test/work-item.test.mjs.bak +548 -0
  68. package/var/kgc/test-atomic-log.json +519 -0
  69. package/var/kgc/test-cascading-log.json +145 -0
  70. package/vitest.config.mjs +18 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * @fileoverview Tests for CRDT (Conflict-free Replicated Data Types)
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ createTimestamp,
8
+ compareTimestamps,
9
+ createLWWRegister,
10
+ updateLWWRegister,
11
+ mergeLWWRegisters,
12
+ threeWayMergeLWW,
13
+ createORSet,
14
+ addToORSet,
15
+ removeFromORSet,
16
+ getORSetValues,
17
+ mergeORSets,
18
+ threeWayMergeORSet,
19
+ } from '../src/crdt.mjs';
20
+
21
+ describe('CRDT - Timestamps', () => {
22
+ it('should create and compare timestamps correctly', () => {
23
+ const t1 = createTimestamp(1, 'actor1');
24
+ const t2 = createTimestamp(2, 'actor1');
25
+ const t3 = createTimestamp(1, 'actor2');
26
+
27
+ expect(compareTimestamps(t1, t2)).toBeLessThan(0); // t1 < t2
28
+ expect(compareTimestamps(t2, t1)).toBeGreaterThan(0); // t2 > t1
29
+ expect(compareTimestamps(t1, t1)).toBe(0); // t1 == t1
30
+
31
+ // Same counter, different actor - lexicographic comparison
32
+ const cmp = compareTimestamps(t1, t3);
33
+ expect(cmp).toBe('actor1'.localeCompare('actor2'));
34
+ });
35
+ });
36
+
37
+ describe('CRDT - LWW-Register', () => {
38
+ it('should create and update LWW-Register', () => {
39
+ const t1 = createTimestamp(1, 'agent1');
40
+ const reg1 = createLWWRegister('initial value', t1);
41
+
42
+ expect(reg1.value).toBe('initial value');
43
+ expect(reg1.timestamp).toEqual(t1);
44
+
45
+ // Update with later timestamp
46
+ const t2 = createTimestamp(2, 'agent1');
47
+ const reg2 = updateLWWRegister(reg1, 'updated value', t2);
48
+
49
+ expect(reg2.value).toBe('updated value');
50
+ expect(reg2.timestamp).toEqual(t2);
51
+
52
+ // Update with earlier timestamp (should be ignored)
53
+ const t0 = createTimestamp(0, 'agent1');
54
+ const reg3 = updateLWWRegister(reg2, 'old value', t0);
55
+
56
+ expect(reg3.value).toBe('updated value'); // Not changed
57
+ expect(reg3.timestamp).toEqual(t2);
58
+ });
59
+
60
+ it('should merge LWW-Registers with last-write-wins semantics', () => {
61
+ const t1 = createTimestamp(1, 'agent1');
62
+ const t2 = createTimestamp(2, 'agent2');
63
+
64
+ const reg1 = createLWWRegister('value from agent1', t1);
65
+ const reg2 = createLWWRegister('value from agent2', t2);
66
+
67
+ // Merge - later timestamp wins
68
+ const merged = mergeLWWRegisters(reg1, reg2);
69
+
70
+ expect(merged.value).toBe('value from agent2');
71
+ expect(merged.timestamp).toEqual(t2);
72
+
73
+ // Merge in opposite order - result should be same
74
+ const merged2 = mergeLWWRegisters(reg2, reg1);
75
+
76
+ expect(merged2.value).toBe('value from agent2');
77
+ expect(merged2.timestamp).toEqual(t2);
78
+ });
79
+
80
+ it('should perform three-way merge with common ancestor', () => {
81
+ const t0 = createTimestamp(0, 'agent0');
82
+ const t1 = createTimestamp(1, 'agent1');
83
+ const t2 = createTimestamp(2, 'agent2');
84
+
85
+ const ancestor = createLWWRegister('original', t0);
86
+ const left = createLWWRegister('left edit', t1);
87
+ const right = createLWWRegister('right edit', t2);
88
+
89
+ // Both branches modified - use LWW
90
+ const merged = threeWayMergeLWW(ancestor, left, right);
91
+
92
+ expect(merged.value).toBe('right edit'); // Later timestamp wins
93
+
94
+ // Only one branch modified
95
+ const merged2 = threeWayMergeLWW(ancestor, left, ancestor);
96
+ expect(merged2.value).toBe('left edit'); // Left branch wins
97
+
98
+ const merged3 = threeWayMergeLWW(ancestor, ancestor, right);
99
+ expect(merged3.value).toBe('right edit'); // Right branch wins
100
+
101
+ // No changes
102
+ const merged4 = threeWayMergeLWW(ancestor, ancestor, ancestor);
103
+ expect(merged4.value).toBe('original'); // No changes
104
+ });
105
+ });
106
+
107
+ describe('CRDT - OR-Set', () => {
108
+ it('should add and retrieve elements from OR-Set', () => {
109
+ let set = createORSet();
110
+ expect(getORSetValues(set)).toHaveLength(0);
111
+
112
+ // Add elements
113
+ const t1 = createTimestamp(1, 'agent1');
114
+ set = addToORSet(set, 'apple', t1);
115
+
116
+ const t2 = createTimestamp(2, 'agent1');
117
+ set = addToORSet(set, 'banana', t2);
118
+
119
+ const values = getORSetValues(set);
120
+ expect(values).toHaveLength(2);
121
+ expect(values).toContain('apple');
122
+ expect(values).toContain('banana');
123
+ });
124
+
125
+ it('should remove elements from OR-Set', () => {
126
+ let set = createORSet();
127
+
128
+ const t1 = createTimestamp(1, 'agent1');
129
+ set = addToORSet(set, 'item1', t1);
130
+
131
+ const t2 = createTimestamp(2, 'agent1');
132
+ set = addToORSet(set, 'item2', t2);
133
+
134
+ expect(getORSetValues(set)).toHaveLength(2);
135
+
136
+ // Remove item1
137
+ const t3 = createTimestamp(3, 'agent1');
138
+ set = removeFromORSet(set, 'item1', t3);
139
+
140
+ const values = getORSetValues(set);
141
+ expect(values).toHaveLength(1);
142
+ expect(values).toContain('item2');
143
+ expect(values).not.toContain('item1');
144
+ });
145
+
146
+ it('should merge OR-Sets correctly', () => {
147
+ // Agent1's set
148
+ let set1 = createORSet();
149
+ const t1 = createTimestamp(1, 'agent1');
150
+ set1 = addToORSet(set1, 'apple', t1);
151
+ const t2 = createTimestamp(2, 'agent1');
152
+ set1 = addToORSet(set1, 'banana', t2);
153
+
154
+ // Agent2's set
155
+ let set2 = createORSet();
156
+ const t3 = createTimestamp(1, 'agent2');
157
+ set2 = addToORSet(set2, 'cherry', t3);
158
+ const t4 = createTimestamp(2, 'agent2');
159
+ set2 = addToORSet(set2, 'banana', t4); // Also adds banana
160
+
161
+ // Merge sets
162
+ const merged = mergeORSets(set1, set2);
163
+ const values = getORSetValues(merged);
164
+
165
+ expect(values).toHaveLength(3);
166
+ expect(values).toContain('apple');
167
+ expect(values).toContain('banana');
168
+ expect(values).toContain('cherry');
169
+
170
+ // Test concurrent add/remove
171
+ let set3 = createORSet();
172
+ const t5 = createTimestamp(1, 'agent3');
173
+ set3 = addToORSet(set3, 'item', t5);
174
+ const t6 = createTimestamp(2, 'agent3');
175
+ set3 = removeFromORSet(set3, 'item', t6);
176
+
177
+ let set4 = createORSet();
178
+ const t7 = createTimestamp(3, 'agent4');
179
+ set4 = addToORSet(set4, 'item', t7); // Re-add after removal
180
+
181
+ const merged2 = mergeORSets(set3, set4);
182
+ const values2 = getORSetValues(merged2);
183
+
184
+ // Item should be present (re-added after remove)
185
+ expect(values2).toContain('item');
186
+ });
187
+
188
+ it('should perform three-way merge for OR-Sets', () => {
189
+ // Ancestor
190
+ let ancestor = createORSet();
191
+ const t0 = createTimestamp(0, 'agent0');
192
+ ancestor = addToORSet(ancestor, 'original', t0);
193
+
194
+ // Left branch - adds 'left'
195
+ let left = createORSet();
196
+ left = addToORSet(left, 'original', t0);
197
+ const t1 = createTimestamp(1, 'agent1');
198
+ left = addToORSet(left, 'left', t1);
199
+
200
+ // Right branch - adds 'right'
201
+ let right = createORSet();
202
+ right = addToORSet(right, 'original', t0);
203
+ const t2 = createTimestamp(1, 'agent2');
204
+ right = addToORSet(right, 'right', t2);
205
+
206
+ // Three-way merge
207
+ const merged = threeWayMergeORSet(ancestor, left, right);
208
+ const values = getORSetValues(merged);
209
+
210
+ expect(values).toHaveLength(3);
211
+ expect(values).toContain('original');
212
+ expect(values).toContain('left');
213
+ expect(values).toContain('right');
214
+ });
215
+ });
@@ -0,0 +1,487 @@
1
+ /**
2
+ * @file Enhanced bounds tests - Soft limits, quotas, rate limiting
3
+ */
4
+
5
+ import { describe, it, beforeEach } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { EnhancedBoundsChecker } from '../src/enhanced-bounds.mjs';
8
+
9
+ // =============================================================================
10
+ // Soft Limits Tests
11
+ // =============================================================================
12
+
13
+ describe('Enhanced Bounds - Soft Limits', () => {
14
+ /** @type {EnhancedBoundsChecker} */
15
+ let checker;
16
+
17
+ beforeEach(() => {
18
+ checker = new EnhancedBoundsChecker({
19
+ max_files_touched: 100,
20
+ max_bytes_changed: 10000,
21
+ max_tool_ops: 1000,
22
+ warnings: {
23
+ filesThreshold: 0.8,
24
+ bytesThreshold: 0.9,
25
+ opsThreshold: 0.75,
26
+ },
27
+ });
28
+ });
29
+
30
+ it('should emit warning receipt at 80% threshold', async () => {
31
+ // Use 80 files (80% of 100)
32
+ const receipt = await checker.checkAndRecord({
33
+ type: 'file_touch',
34
+ files: 80,
35
+ });
36
+
37
+ assert.equal(receipt.admit, true);
38
+ assert.equal(receipt.warnings.length, 1);
39
+ assert.equal(receipt.warnings[0].type, 'warning');
40
+ assert.match(receipt.warnings[0].message, /80%/);
41
+ });
42
+
43
+ it('should emit error receipt at 100% threshold', async () => {
44
+ // Use 100 files (100% of 100)
45
+ const receipt = await checker.checkAndRecord({
46
+ type: 'file_touch',
47
+ files: 100,
48
+ });
49
+
50
+ assert.equal(receipt.admit, true); // First time should admit
51
+ assert.equal(receipt.warnings.length, 0);
52
+ assert.equal(receipt.errors.length, 0);
53
+
54
+ // Next operation should fail
55
+ const receipt2 = await checker.checkAndRecord({
56
+ type: 'file_touch',
57
+ files: 1,
58
+ });
59
+
60
+ assert.equal(receipt2.admit, false);
61
+ assert.equal(receipt2.errors.length, 1);
62
+ assert.equal(receipt2.errors[0].type, 'error');
63
+ });
64
+
65
+ it('should emit warning for bytes at 90% threshold', async () => {
66
+ // Use 9000 bytes (90% of 10000)
67
+ const receipt = await checker.checkAndRecord({
68
+ type: 'bytes_change',
69
+ bytes: 9000,
70
+ });
71
+
72
+ assert.equal(receipt.admit, true);
73
+ assert.equal(receipt.warnings.length, 1);
74
+ assert.equal(receipt.warnings[0].metric, 'bytes');
75
+ assert.equal(receipt.warnings[0].percentage, 90);
76
+ });
77
+
78
+ it('should emit warning for ops at 75% threshold', async () => {
79
+ // Use 750 ops (75% of 1000)
80
+ const receipt = await checker.checkAndRecord({
81
+ type: 'tool_op',
82
+ ops: 750,
83
+ });
84
+
85
+ assert.equal(receipt.admit, true);
86
+ assert.equal(receipt.warnings.length, 1);
87
+ assert.equal(receipt.warnings[0].metric, 'ops');
88
+ assert.match(receipt.warnings[0].message, /75%/);
89
+ });
90
+ });
91
+
92
+ // =============================================================================
93
+ // Per-Agent Quota Tests
94
+ // =============================================================================
95
+
96
+ describe('Enhanced Bounds - Per-Agent Quotas', () => {
97
+ /** @type {EnhancedBoundsChecker} */
98
+ let checker;
99
+
100
+ beforeEach(() => {
101
+ checker = new EnhancedBoundsChecker({
102
+ max_files_touched: 1000,
103
+ max_bytes_changed: 100000,
104
+ });
105
+ });
106
+
107
+ it('should enforce per-agent file quota', async () => {
108
+ checker.setAgentQuota('agent:backend-dev', {
109
+ max_files: 10,
110
+ max_bytes: 5000,
111
+ });
112
+
113
+ // Agent can use up to 10 files
114
+ const receipt1 = await checker.checkAndRecord(
115
+ {
116
+ type: 'file_touch',
117
+ files: 5,
118
+ },
119
+ 'agent:backend-dev'
120
+ );
121
+ assert.equal(receipt1.admit, true);
122
+
123
+ const receipt2 = await checker.checkAndRecord(
124
+ {
125
+ type: 'file_touch',
126
+ files: 4,
127
+ },
128
+ 'agent:backend-dev'
129
+ );
130
+ assert.equal(receipt2.admit, true);
131
+
132
+ // Should reject when quota exceeded
133
+ const receipt3 = await checker.checkAndRecord(
134
+ {
135
+ type: 'file_touch',
136
+ files: 2,
137
+ },
138
+ 'agent:backend-dev'
139
+ );
140
+ assert.equal(receipt3.admit, false);
141
+ assert.equal(receipt3.errors.length, 1);
142
+ assert.match(receipt3.errors[0].message, /quota/i);
143
+ });
144
+
145
+ it('should enforce per-agent byte quota', async () => {
146
+ checker.setAgentQuota('agent:tester', {
147
+ max_bytes: 1000,
148
+ });
149
+
150
+ const receipt1 = await checker.checkAndRecord(
151
+ {
152
+ type: 'bytes_change',
153
+ bytes: 800,
154
+ },
155
+ 'agent:tester'
156
+ );
157
+ assert.equal(receipt1.admit, true);
158
+
159
+ const receipt2 = await checker.checkAndRecord(
160
+ {
161
+ type: 'bytes_change',
162
+ bytes: 300,
163
+ },
164
+ 'agent:tester'
165
+ );
166
+ assert.equal(receipt2.admit, false);
167
+ });
168
+
169
+ it('should track separate quotas for different agents', async () => {
170
+ checker.setAgentQuota('agent:A', { max_files: 5 });
171
+ checker.setAgentQuota('agent:B', { max_files: 10 });
172
+
173
+ // Agent A uses 5 files
174
+ await checker.checkAndRecord({ type: 'file_touch', files: 5 }, 'agent:A');
175
+
176
+ // Agent B can still use 10 files
177
+ const receipt = await checker.checkAndRecord(
178
+ { type: 'file_touch', files: 8 },
179
+ 'agent:B'
180
+ );
181
+ assert.equal(receipt.admit, true);
182
+
183
+ // Agent A cannot use more
184
+ const receipt2 = await checker.checkAndRecord(
185
+ { type: 'file_touch', files: 1 },
186
+ 'agent:A'
187
+ );
188
+ assert.equal(receipt2.admit, false);
189
+ });
190
+ });
191
+
192
+ // =============================================================================
193
+ // Rate Limiting Tests
194
+ // =============================================================================
195
+
196
+ describe('Enhanced Bounds - Rate Limiting', () => {
197
+ /** @type {EnhancedBoundsChecker} */
198
+ let checker;
199
+
200
+ beforeEach(() => {
201
+ checker = new EnhancedBoundsChecker({
202
+ max_files_touched: 1000,
203
+ });
204
+ });
205
+
206
+ it('should enforce rate limit per agent', async () => {
207
+ checker.setRateLimit({
208
+ maxOpsPerSecond: 5,
209
+ windowSizeMs: 1000,
210
+ });
211
+
212
+ // First 5 operations should succeed
213
+ for (let i = 0; i < 5; i++) {
214
+ const receipt = await checker.checkAndRecord(
215
+ { type: 'op', files: 1 },
216
+ 'agent:worker'
217
+ );
218
+ assert.equal(receipt.admit, true);
219
+ }
220
+
221
+ // 6th operation should be rate limited
222
+ const receipt = await checker.checkAndRecord(
223
+ { type: 'op', files: 1 },
224
+ 'agent:worker'
225
+ );
226
+ assert.equal(receipt.admit, false);
227
+ assert.ok(receipt.errors.some((e) => e.metric === 'rate_limit'));
228
+ });
229
+
230
+ it('should allow operations after window expires', async () => {
231
+ checker.setRateLimit({
232
+ maxOpsPerSecond: 3,
233
+ windowSizeMs: 100, // Short window for testing
234
+ });
235
+
236
+ // Fill the window
237
+ for (let i = 0; i < 3; i++) {
238
+ await checker.checkAndRecord({ type: 'op', files: 1 }, 'agent:test');
239
+ }
240
+
241
+ // Should be rate limited
242
+ const receipt1 = await checker.checkAndRecord(
243
+ { type: 'op', files: 1 },
244
+ 'agent:test'
245
+ );
246
+ assert.equal(receipt1.admit, false);
247
+
248
+ // Wait for window to expire
249
+ await new Promise((resolve) => setTimeout(resolve, 150));
250
+
251
+ // Should now succeed
252
+ const receipt2 = await checker.checkAndRecord(
253
+ { type: 'op', files: 1 },
254
+ 'agent:test'
255
+ );
256
+ assert.equal(receipt2.admit, true);
257
+ });
258
+ });
259
+
260
+ // =============================================================================
261
+ // Integration Tests
262
+ // =============================================================================
263
+
264
+ describe('Enhanced Bounds - Integration', () => {
265
+ /** @type {EnhancedBoundsChecker} */
266
+ let checker;
267
+
268
+ beforeEach(() => {
269
+ checker = new EnhancedBoundsChecker({
270
+ max_files_touched: 100,
271
+ max_bytes_changed: 10000,
272
+ max_tool_ops: 1000,
273
+ warnings: {
274
+ filesThreshold: 0.8,
275
+ bytesThreshold: 0.9,
276
+ },
277
+ });
278
+
279
+ checker.setAgentQuota('agent:backend-dev', {
280
+ max_files: 50,
281
+ max_bytes: 5000,
282
+ });
283
+
284
+ checker.setRateLimit({
285
+ maxOpsPerSecond: 10,
286
+ windowSizeMs: 1000,
287
+ });
288
+ });
289
+
290
+ it('should enforce all limits simultaneously', async () => {
291
+ // This should pass (within all limits)
292
+ const receipt1 = await checker.checkAndRecord(
293
+ {
294
+ type: 'composite',
295
+ files: 40,
296
+ bytes: 4000,
297
+ ops: 1,
298
+ },
299
+ 'agent:backend-dev'
300
+ );
301
+ assert.equal(receipt1.admit, true);
302
+ assert.equal(receipt1.warnings.length, 0);
303
+
304
+ // This should trigger soft limit warning (80% of 50 files = 40 files already used)
305
+ const receipt2 = await checker.checkAndRecord(
306
+ {
307
+ type: 'composite',
308
+ files: 5,
309
+ bytes: 500,
310
+ ops: 1,
311
+ },
312
+ 'agent:backend-dev'
313
+ );
314
+ assert.equal(receipt2.admit, true);
315
+ // May have warnings for global or agent limits
316
+
317
+ // This should fail agent quota
318
+ const receipt3 = await checker.checkAndRecord(
319
+ {
320
+ type: 'composite',
321
+ files: 10,
322
+ bytes: 100,
323
+ ops: 1,
324
+ },
325
+ 'agent:backend-dev'
326
+ );
327
+ assert.equal(receipt3.admit, false);
328
+ });
329
+
330
+ it('should provide detailed receipt with all checks', async () => {
331
+ const receipt = await checker.checkAndRecord(
332
+ {
333
+ type: 'file_write',
334
+ files: 1,
335
+ bytes: 100,
336
+ ops: 1,
337
+ },
338
+ 'agent:backend-dev'
339
+ );
340
+
341
+ assert.ok(receipt.receipt_id);
342
+ assert.ok(receipt.timestamp);
343
+ assert.equal(receipt.agentId, 'agent:backend-dev');
344
+ assert.equal(receipt.operation, 'file_write');
345
+ assert.ok(Array.isArray(receipt.warnings));
346
+ assert.ok(Array.isArray(receipt.errors));
347
+ assert.equal(typeof receipt.admit, 'boolean');
348
+ });
349
+
350
+ it('should track usage correctly', async () => {
351
+ await checker.checkAndRecord(
352
+ {
353
+ type: 'op1',
354
+ files: 10,
355
+ bytes: 1000,
356
+ ops: 5,
357
+ },
358
+ 'agent:backend-dev'
359
+ );
360
+
361
+ const globalUsage = checker.getUsage();
362
+ assert.equal(globalUsage.files_touched, 10);
363
+ assert.equal(globalUsage.bytes_changed, 1000);
364
+ assert.equal(globalUsage.tool_ops, 5);
365
+
366
+ const agentUsage = checker.getAgentUsage('agent:backend-dev');
367
+ assert.equal(agentUsage.files_touched, 10);
368
+ assert.equal(agentUsage.bytes_changed, 1000);
369
+ assert.equal(agentUsage.tool_ops, 5);
370
+ });
371
+
372
+ it('should filter receipts by type', async () => {
373
+ // Create warning
374
+ await checker.checkAndRecord({ type: 'op', files: 80 });
375
+
376
+ // Create error
377
+ checker.setAgentQuota('agent:test', { max_files: 1 });
378
+ await checker.checkAndRecord({ type: 'op', files: 10 }, 'agent:test');
379
+
380
+ const warnings = checker.getReceiptsByType('warning');
381
+ const errors = checker.getReceiptsByType('error');
382
+
383
+ assert.ok(warnings.length > 0);
384
+ assert.ok(errors.length > 0);
385
+ });
386
+ });
387
+
388
+ // =============================================================================
389
+ // Usage Tracking Tests
390
+ // =============================================================================
391
+
392
+ describe('Enhanced Bounds - Usage Tracking', () => {
393
+ it('should track global usage across all agents', async () => {
394
+ const checker = new EnhancedBoundsChecker({
395
+ max_files_touched: 1000,
396
+ max_bytes_changed: 100000,
397
+ });
398
+
399
+ await checker.checkAndRecord({ type: 'op', files: 10 }, 'agent:A');
400
+ await checker.checkAndRecord({ type: 'op', files: 20 }, 'agent:B');
401
+ await checker.checkAndRecord({ type: 'op', bytes: 1000 }, 'agent:A');
402
+
403
+ const usage = checker.getUsage();
404
+ assert.equal(usage.files_touched, 30);
405
+ assert.equal(usage.bytes_changed, 1000);
406
+ });
407
+
408
+ it('should track per-agent usage separately', async () => {
409
+ const checker = new EnhancedBoundsChecker({
410
+ max_files_touched: 1000,
411
+ });
412
+
413
+ await checker.checkAndRecord({ type: 'op', files: 10 }, 'agent:A');
414
+ await checker.checkAndRecord({ type: 'op', files: 20 }, 'agent:B');
415
+
416
+ const usageA = checker.getAgentUsage('agent:A');
417
+ const usageB = checker.getAgentUsage('agent:B');
418
+
419
+ assert.equal(usageA.files_touched, 10);
420
+ assert.equal(usageB.files_touched, 20);
421
+ });
422
+
423
+ it('should reset usage correctly', async () => {
424
+ const checker = new EnhancedBoundsChecker({
425
+ max_files_touched: 100,
426
+ });
427
+
428
+ await checker.checkAndRecord({ type: 'op', files: 50 }, 'agent:test');
429
+
430
+ checker.reset();
431
+
432
+ const usage = checker.getUsage();
433
+ assert.equal(usage.files_touched, 0);
434
+ assert.equal(checker.getReceipts().length, 0);
435
+ });
436
+ });
437
+
438
+ // =============================================================================
439
+ // Custom Warning Thresholds Tests
440
+ // =============================================================================
441
+
442
+ describe('Enhanced Bounds - Custom Warning Thresholds', () => {
443
+ it('should use custom warning thresholds', async () => {
444
+ const checker = new EnhancedBoundsChecker({
445
+ max_files_touched: 100,
446
+ warnings: {
447
+ filesThreshold: 0.5, // Warn at 50%
448
+ },
449
+ });
450
+
451
+ // At 50%, should warn
452
+ const receipt1 = await checker.checkAndRecord({
453
+ type: 'op',
454
+ files: 50,
455
+ });
456
+ assert.equal(receipt1.warnings.length, 1);
457
+ assert.equal(receipt1.warnings[0].percentage, 50);
458
+
459
+ // Below 50%, should not warn
460
+ checker.reset();
461
+ const receipt2 = await checker.checkAndRecord({
462
+ type: 'op',
463
+ files: 40,
464
+ });
465
+ assert.equal(receipt2.warnings.length, 0);
466
+ });
467
+
468
+ it('should support different thresholds for different metrics', async () => {
469
+ const checker = new EnhancedBoundsChecker({
470
+ max_files_touched: 100,
471
+ max_bytes_changed: 10000,
472
+ warnings: {
473
+ filesThreshold: 0.5,
474
+ bytesThreshold: 0.9,
475
+ },
476
+ });
477
+
478
+ const receipt = await checker.checkAndRecord({
479
+ type: 'op',
480
+ files: 50, // 50% - should warn
481
+ bytes: 8000, // 80% - should not warn
482
+ });
483
+
484
+ assert.equal(receipt.warnings.length, 1);
485
+ assert.equal(receipt.warnings[0].metric, 'files');
486
+ });
487
+ });