@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,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for Atomic Capsule Admission
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import { AtomicAdmissionGate, cascadingRollback } from '../src/atomic-admission.mjs';
|
|
7
|
+
|
|
8
|
+
const createCapsule = (id, o_hash, file_edits) => ({
|
|
9
|
+
id,
|
|
10
|
+
o_hash,
|
|
11
|
+
file_edits,
|
|
12
|
+
metadata: { created_at: new Date().toISOString() },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const createFileEdit = (file_path, line_start, line_end, content, operation = 'replace') => ({
|
|
16
|
+
file_path,
|
|
17
|
+
line_start,
|
|
18
|
+
line_end,
|
|
19
|
+
content,
|
|
20
|
+
operation,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const defaultTotalOrder = {
|
|
24
|
+
rules: [],
|
|
25
|
+
default_rule: {
|
|
26
|
+
strategy: 'earlier_wins',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe('AtomicAdmissionGate', () => {
|
|
31
|
+
let gate;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
gate = new AtomicAdmissionGate({ logPath: './var/kgc/test-atomic-log.json' });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should admit all capsules atomically when no conflicts', async () => {
|
|
38
|
+
const capsules = [
|
|
39
|
+
createCapsule('c1', 'hash1', [
|
|
40
|
+
createFileEdit('file1.js', 1, 5, 'content1'),
|
|
41
|
+
]),
|
|
42
|
+
createCapsule('c2', 'hash2', [
|
|
43
|
+
createFileEdit('file2.js', 1, 5, 'content2'),
|
|
44
|
+
]),
|
|
45
|
+
createCapsule('c3', 'hash3', [
|
|
46
|
+
createFileEdit('file3.js', 1, 5, 'content3'),
|
|
47
|
+
]),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const result = await gate.admitCapsules(capsules, defaultTotalOrder);
|
|
51
|
+
|
|
52
|
+
expect(result.success).toBe(true);
|
|
53
|
+
expect(result.admitted).toHaveLength(3);
|
|
54
|
+
expect(result.admitted).toContain('c1');
|
|
55
|
+
expect(result.admitted).toContain('c2');
|
|
56
|
+
expect(result.admitted).toContain('c3');
|
|
57
|
+
expect(result.denied).toHaveLength(0);
|
|
58
|
+
expect(result.transaction_id).toBeDefined();
|
|
59
|
+
expect(result.receipts).toBeDefined();
|
|
60
|
+
expect(result.receipts).toHaveLength(3);
|
|
61
|
+
|
|
62
|
+
// Verify capsules are admitted
|
|
63
|
+
expect(gate.isAdmitted('c1')).toBe(true);
|
|
64
|
+
expect(gate.isAdmitted('c2')).toBe(true);
|
|
65
|
+
expect(gate.isAdmitted('c3')).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should reject all capsules atomically when conflicts detected', async () => {
|
|
69
|
+
const capsules = [
|
|
70
|
+
createCapsule('c1', 'hash_a', [
|
|
71
|
+
createFileEdit('file.js', 1, 10, 'content1'),
|
|
72
|
+
]),
|
|
73
|
+
createCapsule('c2', 'hash_b', [
|
|
74
|
+
createFileEdit('file.js', 5, 15, 'content2'), // Conflict with c1
|
|
75
|
+
]),
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const result = await gate.admitCapsules(capsules, defaultTotalOrder);
|
|
79
|
+
|
|
80
|
+
expect(result.success).toBe(false);
|
|
81
|
+
expect(result.admitted).toHaveLength(0);
|
|
82
|
+
expect(result.denied).toHaveLength(1); // One capsule denied due to conflict
|
|
83
|
+
expect(result.conflict_receipts).toHaveLength(1);
|
|
84
|
+
expect(result.errors).toBeDefined();
|
|
85
|
+
expect(result.errors[0]).toContain('Conflict detected');
|
|
86
|
+
|
|
87
|
+
// Verify no capsules admitted
|
|
88
|
+
expect(gate.isAdmitted('c1')).toBe(false);
|
|
89
|
+
expect(gate.isAdmitted('c2')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should rollback transaction and remove admitted capsules', async () => {
|
|
93
|
+
const capsules = [
|
|
94
|
+
createCapsule('c1', 'hash1', [
|
|
95
|
+
createFileEdit('file1.js', 1, 5, 'content1'),
|
|
96
|
+
]),
|
|
97
|
+
createCapsule('c2', 'hash2', [
|
|
98
|
+
createFileEdit('file2.js', 1, 5, 'content2'),
|
|
99
|
+
]),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
// Admit capsules
|
|
103
|
+
const admitResult = await gate.admitCapsules(capsules, defaultTotalOrder);
|
|
104
|
+
|
|
105
|
+
expect(admitResult.success).toBe(true);
|
|
106
|
+
expect(gate.isAdmitted('c1')).toBe(true);
|
|
107
|
+
expect(gate.isAdmitted('c2')).toBe(true);
|
|
108
|
+
|
|
109
|
+
// Rollback transaction
|
|
110
|
+
const rollbackResult = await gate.rollbackTransaction(admitResult.transaction_id);
|
|
111
|
+
|
|
112
|
+
expect(rollbackResult.success).toBe(true);
|
|
113
|
+
expect(rollbackResult.undone).toBeGreaterThan(0);
|
|
114
|
+
|
|
115
|
+
// Verify capsules removed from admitted set
|
|
116
|
+
expect(gate.isAdmitted('c1')).toBe(false);
|
|
117
|
+
expect(gate.isAdmitted('c2')).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Cascading Rollback', () => {
|
|
122
|
+
it('should rollback transaction and all dependent transactions', async () => {
|
|
123
|
+
const gate = new AtomicAdmissionGate({ logPath: './var/kgc/test-cascading-log.json' });
|
|
124
|
+
|
|
125
|
+
// Transaction 1
|
|
126
|
+
const capsules1 = [
|
|
127
|
+
createCapsule('c1', 'hash1', [
|
|
128
|
+
createFileEdit('file1.js', 1, 5, 'content1'),
|
|
129
|
+
]),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const result1 = await gate.admitCapsules(capsules1, defaultTotalOrder);
|
|
133
|
+
expect(result1.success).toBe(true);
|
|
134
|
+
|
|
135
|
+
// Transaction 2 (dependent on tx1)
|
|
136
|
+
const capsules2 = [
|
|
137
|
+
createCapsule('c2', 'hash2', [
|
|
138
|
+
createFileEdit('file2.js', 1, 5, 'content2'),
|
|
139
|
+
]),
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const result2 = await gate.admitCapsules(capsules2, defaultTotalOrder);
|
|
143
|
+
expect(result2.success).toBe(true);
|
|
144
|
+
|
|
145
|
+
// Cascading rollback
|
|
146
|
+
const rollbackResult = await cascadingRollback(gate, result1.transaction_id);
|
|
147
|
+
|
|
148
|
+
expect(rollbackResult.success).toBe(true);
|
|
149
|
+
expect(rollbackResult.rolled_back).toContain(result1.transaction_id);
|
|
150
|
+
|
|
151
|
+
// Verify all capsules removed
|
|
152
|
+
expect(gate.isAdmitted('c1')).toBe(false);
|
|
153
|
+
expect(gate.isAdmitted('c2')).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Bounds checker tests - Capacity limits enforcement
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { BoundsChecker, enforceBounds } from '../src/bounds.mjs';
|
|
8
|
+
|
|
9
|
+
describe('BoundsChecker', () => {
|
|
10
|
+
/** @type {BoundsChecker} */
|
|
11
|
+
let checker;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
checker = new BoundsChecker({
|
|
15
|
+
max_files_touched: 10,
|
|
16
|
+
max_bytes_changed: 1000,
|
|
17
|
+
max_tool_ops: 50,
|
|
18
|
+
max_runtime_ms: 5000,
|
|
19
|
+
max_graph_rewrites: 25,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Individual Bounds Enforcement', () => {
|
|
24
|
+
it('should enforce max_files_touched limit', () => {
|
|
25
|
+
for (let i = 0; i < 10; i++) {
|
|
26
|
+
const result = checker.canExecute({ type: 'file_touch', files: 1 });
|
|
27
|
+
assert.equal(result, true, `File ${i + 1} should be allowed`);
|
|
28
|
+
checker.recordOperation({ type: 'file_touch', files: 1 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = checker.canExecute({ type: 'file_touch', files: 1 });
|
|
32
|
+
assert.equal(result, false, 'Should reject file touch when limit reached');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should enforce max_bytes_changed limit', () => {
|
|
36
|
+
const result1 = checker.canExecute({ type: 'bytes_change', bytes: 500 });
|
|
37
|
+
assert.equal(result1, true);
|
|
38
|
+
checker.recordOperation({ type: 'bytes_change', bytes: 500 });
|
|
39
|
+
|
|
40
|
+
const result2 = checker.canExecute({ type: 'bytes_change', bytes: 400 });
|
|
41
|
+
assert.equal(result2, true);
|
|
42
|
+
checker.recordOperation({ type: 'bytes_change', bytes: 400 });
|
|
43
|
+
|
|
44
|
+
const result3 = checker.canExecute({ type: 'bytes_change', bytes: 200 });
|
|
45
|
+
assert.equal(result3, false, 'Should reject when bytes limit exceeded');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should enforce max_tool_ops limit', () => {
|
|
49
|
+
for (let i = 0; i < 50; i++) {
|
|
50
|
+
const result = checker.canExecute({ type: 'tool_op', ops: 1 });
|
|
51
|
+
assert.equal(result, true, `Op ${i + 1} should be allowed`);
|
|
52
|
+
checker.recordOperation({ type: 'tool_op', ops: 1 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = checker.canExecute({ type: 'tool_op', ops: 1 });
|
|
56
|
+
assert.equal(result, false, 'Should reject tool op when limit reached');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should enforce max_runtime_ms limit', () => {
|
|
60
|
+
const result1 = checker.canExecute({ type: 'runtime', ms: 3000 });
|
|
61
|
+
assert.equal(result1, true);
|
|
62
|
+
checker.recordOperation({ type: 'runtime', ms: 3000 });
|
|
63
|
+
|
|
64
|
+
const result2 = checker.canExecute({ type: 'runtime', ms: 1500 });
|
|
65
|
+
assert.equal(result2, true);
|
|
66
|
+
checker.recordOperation({ type: 'runtime', ms: 1500 });
|
|
67
|
+
|
|
68
|
+
const result3 = checker.canExecute({ type: 'runtime', ms: 1000 });
|
|
69
|
+
assert.equal(result3, false, 'Should reject when runtime limit exceeded');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should enforce max_graph_rewrites limit', () => {
|
|
73
|
+
for (let i = 0; i < 25; i++) {
|
|
74
|
+
const result = checker.canExecute({ type: 'graph_rewrite', rewrites: 1 });
|
|
75
|
+
assert.equal(result, true, `Rewrite ${i + 1} should be allowed`);
|
|
76
|
+
checker.recordOperation({ type: 'graph_rewrite', rewrites: 1 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = checker.canExecute({ type: 'graph_rewrite', rewrites: 1 });
|
|
80
|
+
assert.equal(result, false, 'Should reject rewrite when limit reached');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('Cumulative Tracking', () => {
|
|
85
|
+
it('should track cumulative file touches', () => {
|
|
86
|
+
checker.recordOperation({ type: 'file_touch', files: 3 });
|
|
87
|
+
assert.equal(checker.getUsage().files_touched, 3);
|
|
88
|
+
|
|
89
|
+
checker.recordOperation({ type: 'file_touch', files: 5 });
|
|
90
|
+
assert.equal(checker.getUsage().files_touched, 8);
|
|
91
|
+
|
|
92
|
+
checker.recordOperation({ type: 'file_touch', files: 2 });
|
|
93
|
+
assert.equal(checker.getUsage().files_touched, 10);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should track cumulative bytes changed', () => {
|
|
97
|
+
checker.recordOperation({ type: 'bytes_change', bytes: 250 });
|
|
98
|
+
assert.equal(checker.getUsage().bytes_changed, 250);
|
|
99
|
+
|
|
100
|
+
checker.recordOperation({ type: 'bytes_change', bytes: 500 });
|
|
101
|
+
assert.equal(checker.getUsage().bytes_changed, 750);
|
|
102
|
+
|
|
103
|
+
checker.recordOperation({ type: 'bytes_change', bytes: 100 });
|
|
104
|
+
assert.equal(checker.getUsage().bytes_changed, 850);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should track all metrics simultaneously', () => {
|
|
108
|
+
checker.recordOperation({ type: 'file_touch', files: 2 });
|
|
109
|
+
checker.recordOperation({ type: 'bytes_change', bytes: 300 });
|
|
110
|
+
checker.recordOperation({ type: 'tool_op', ops: 10 });
|
|
111
|
+
checker.recordOperation({ type: 'runtime', ms: 1000 });
|
|
112
|
+
checker.recordOperation({ type: 'graph_rewrite', rewrites: 5 });
|
|
113
|
+
|
|
114
|
+
const usage = checker.getUsage();
|
|
115
|
+
assert.equal(usage.files_touched, 2);
|
|
116
|
+
assert.equal(usage.bytes_changed, 300);
|
|
117
|
+
assert.equal(usage.tool_ops, 10);
|
|
118
|
+
assert.equal(usage.runtime_ms, 1000);
|
|
119
|
+
assert.equal(usage.graph_rewrites, 5);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should respect all bounds when checking composite operations', () => {
|
|
123
|
+
checker.recordOperation({ type: 'file_touch', files: 9 });
|
|
124
|
+
checker.recordOperation({ type: 'bytes_change', bytes: 900 });
|
|
125
|
+
|
|
126
|
+
// Should allow operation within both bounds
|
|
127
|
+
const result1 = checker.canExecute({
|
|
128
|
+
type: 'composite',
|
|
129
|
+
files: 1,
|
|
130
|
+
bytes: 50
|
|
131
|
+
});
|
|
132
|
+
assert.equal(result1, true);
|
|
133
|
+
|
|
134
|
+
// Should reject if any bound would be exceeded
|
|
135
|
+
const result2 = checker.canExecute({
|
|
136
|
+
type: 'composite',
|
|
137
|
+
files: 2,
|
|
138
|
+
bytes: 50
|
|
139
|
+
});
|
|
140
|
+
assert.equal(result2, false, 'Should reject when files bound exceeded');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('Stress Tests with Max Values', () => {
|
|
145
|
+
it('should handle max_files=100 stress test', () => {
|
|
146
|
+
const stressChecker = new BoundsChecker({ max_files_touched: 100 });
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < 100; i++) {
|
|
149
|
+
assert.equal(stressChecker.canExecute({ type: 'file_touch', files: 1 }), true);
|
|
150
|
+
stressChecker.recordOperation({ type: 'file_touch', files: 1 });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
assert.equal(stressChecker.getUsage().files_touched, 100);
|
|
154
|
+
assert.equal(stressChecker.canExecute({ type: 'file_touch', files: 1 }), false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle max_bytes=10MB stress test', () => {
|
|
158
|
+
const stressChecker = new BoundsChecker({ max_bytes_changed: 10485760 }); // 10MB
|
|
159
|
+
|
|
160
|
+
// Add 1MB chunks
|
|
161
|
+
for (let i = 0; i < 10; i++) {
|
|
162
|
+
assert.equal(stressChecker.canExecute({ type: 'bytes_change', bytes: 1048576 }), true);
|
|
163
|
+
stressChecker.recordOperation({ type: 'bytes_change', bytes: 1048576 });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
assert.equal(stressChecker.getUsage().bytes_changed, 10485760);
|
|
167
|
+
assert.equal(stressChecker.canExecute({ type: 'bytes_change', bytes: 1 }), false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should handle max_ops=1000 stress test', () => {
|
|
171
|
+
const stressChecker = new BoundsChecker({ max_tool_ops: 1000 });
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < 1000; i++) {
|
|
174
|
+
assert.equal(stressChecker.canExecute({ type: 'tool_op', ops: 1 }), true);
|
|
175
|
+
stressChecker.recordOperation({ type: 'tool_op', ops: 1 });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
assert.equal(stressChecker.getUsage().tool_ops, 1000);
|
|
179
|
+
assert.equal(stressChecker.canExecute({ type: 'tool_op', ops: 1 }), false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should handle max_runtime=300000ms stress test', () => {
|
|
183
|
+
const stressChecker = new BoundsChecker({ max_runtime_ms: 300000 }); // 5 minutes
|
|
184
|
+
|
|
185
|
+
// Add 30 second chunks
|
|
186
|
+
for (let i = 0; i < 10; i++) {
|
|
187
|
+
assert.equal(stressChecker.canExecute({ type: 'runtime', ms: 30000 }), true);
|
|
188
|
+
stressChecker.recordOperation({ type: 'runtime', ms: 30000 });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
assert.equal(stressChecker.getUsage().runtime_ms, 300000);
|
|
192
|
+
assert.equal(stressChecker.canExecute({ type: 'runtime', ms: 1 }), false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle max_rewrites=500 stress test', () => {
|
|
196
|
+
const stressChecker = new BoundsChecker({ max_graph_rewrites: 500 });
|
|
197
|
+
|
|
198
|
+
for (let i = 0; i < 500; i++) {
|
|
199
|
+
assert.equal(stressChecker.canExecute({ type: 'graph_rewrite', rewrites: 1 }), true);
|
|
200
|
+
stressChecker.recordOperation({ type: 'graph_rewrite', rewrites: 1 });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
assert.equal(stressChecker.getUsage().graph_rewrites, 500);
|
|
204
|
+
assert.equal(stressChecker.canExecute({ type: 'graph_rewrite', rewrites: 1 }), false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('enforceBounds', () => {
|
|
210
|
+
const bounds = {
|
|
211
|
+
max_files_touched: 5,
|
|
212
|
+
max_bytes_changed: 500,
|
|
213
|
+
max_tool_ops: 20,
|
|
214
|
+
max_runtime_ms: 2000,
|
|
215
|
+
max_graph_rewrites: 10,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
describe('Receipt Generation', () => {
|
|
219
|
+
it('should return admission receipt when within bounds', () => {
|
|
220
|
+
const operation = { type: 'file_touch', files: 2 };
|
|
221
|
+
const receipt = enforceBounds(operation, bounds);
|
|
222
|
+
|
|
223
|
+
assert.equal(receipt.admit, true);
|
|
224
|
+
assert.ok(receipt.bound_used, 'Should specify bound used');
|
|
225
|
+
assert.ok(receipt.receipt_id, 'Should have receipt ID');
|
|
226
|
+
assert.ok(receipt.timestamp, 'Should have timestamp');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should return rejection receipt when exceeding bounds', () => {
|
|
230
|
+
const operation = { type: 'file_touch', files: 10 };
|
|
231
|
+
const receipt = enforceBounds(operation, bounds);
|
|
232
|
+
|
|
233
|
+
assert.equal(receipt.admit, false);
|
|
234
|
+
assert.ok(receipt.reason, 'Should provide rejection reason');
|
|
235
|
+
assert.equal(receipt.bound_violated, 'files');
|
|
236
|
+
assert.ok(receipt.receipt_id, 'Should have receipt ID');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should generate unique receipt IDs', () => {
|
|
240
|
+
const receipt1 = enforceBounds({ type: 'file_touch', files: 1 }, bounds);
|
|
241
|
+
const receipt2 = enforceBounds({ type: 'file_touch', files: 1 }, bounds);
|
|
242
|
+
|
|
243
|
+
assert.notEqual(receipt1.receipt_id, receipt2.receipt_id);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should include parent_receipt_id when provided', () => {
|
|
247
|
+
const parentId = '123e4567-e89b-12d3-a456-426614174000';
|
|
248
|
+
const receipt = enforceBounds(
|
|
249
|
+
{ type: 'file_touch', files: 1 },
|
|
250
|
+
bounds,
|
|
251
|
+
{ parent_receipt_id: parentId }
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
assert.equal(receipt.parent_receipt_id, parentId);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should create chainable receipts', () => {
|
|
258
|
+
const receipt1 = enforceBounds({ type: 'file_touch', files: 1 }, bounds);
|
|
259
|
+
const receipt2 = enforceBounds(
|
|
260
|
+
{ type: 'bytes_change', bytes: 100 },
|
|
261
|
+
bounds,
|
|
262
|
+
{ parent_receipt_id: receipt1.receipt_id }
|
|
263
|
+
);
|
|
264
|
+
const receipt3 = enforceBounds(
|
|
265
|
+
{ type: 'tool_op', ops: 5 },
|
|
266
|
+
bounds,
|
|
267
|
+
{ parent_receipt_id: receipt2.receipt_id }
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
assert.equal(receipt2.parent_receipt_id, receipt1.receipt_id);
|
|
271
|
+
assert.equal(receipt3.parent_receipt_id, receipt2.receipt_id);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('Denial Receipts', () => {
|
|
276
|
+
it('should generate receipt when files limit exceeded', () => {
|
|
277
|
+
const receipt = enforceBounds({ type: 'file_touch', files: 100 }, bounds);
|
|
278
|
+
|
|
279
|
+
assert.equal(receipt.admit, false);
|
|
280
|
+
assert.equal(receipt.bound_violated, 'files');
|
|
281
|
+
assert.match(receipt.reason, /exceeded max_files/i);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should generate receipt when bytes limit exceeded', () => {
|
|
285
|
+
const receipt = enforceBounds({ type: 'bytes_change', bytes: 1000 }, bounds);
|
|
286
|
+
|
|
287
|
+
assert.equal(receipt.admit, false);
|
|
288
|
+
assert.equal(receipt.bound_violated, 'bytes');
|
|
289
|
+
assert.match(receipt.reason, /exceeded max_bytes/i);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should generate receipt when ops limit exceeded', () => {
|
|
293
|
+
const receipt = enforceBounds({ type: 'tool_op', ops: 50 }, bounds);
|
|
294
|
+
|
|
295
|
+
assert.equal(receipt.admit, false);
|
|
296
|
+
assert.equal(receipt.bound_violated, 'ops');
|
|
297
|
+
assert.match(receipt.reason, /exceeded max_tool_ops/i);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should generate receipt when runtime limit exceeded', () => {
|
|
301
|
+
const receipt = enforceBounds({ type: 'runtime', ms: 5000 }, bounds);
|
|
302
|
+
|
|
303
|
+
assert.equal(receipt.admit, false);
|
|
304
|
+
assert.equal(receipt.bound_violated, 'runtime');
|
|
305
|
+
assert.match(receipt.reason, /exceeded max_runtime/i);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should generate receipt when rewrites limit exceeded', () => {
|
|
309
|
+
const receipt = enforceBounds({ type: 'graph_rewrite', rewrites: 20 }, bounds);
|
|
310
|
+
|
|
311
|
+
assert.equal(receipt.admit, false);
|
|
312
|
+
assert.equal(receipt.bound_violated, 'rewrites');
|
|
313
|
+
assert.match(receipt.reason, /exceeded max_graph_rewrites/i);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('Stress Test Receipts', () => {
|
|
318
|
+
it('should handle high-volume receipt generation', () => {
|
|
319
|
+
const stressBounds = {
|
|
320
|
+
max_files_touched: 1000,
|
|
321
|
+
max_bytes_changed: 10485760,
|
|
322
|
+
max_tool_ops: 1000,
|
|
323
|
+
max_runtime_ms: 300000,
|
|
324
|
+
max_graph_rewrites: 500,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const receipts = [];
|
|
328
|
+
for (let i = 0; i < 100; i++) {
|
|
329
|
+
const receipt = enforceBounds({ type: 'file_touch', files: 1 }, stressBounds);
|
|
330
|
+
receipts.push(receipt);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// All should be admitted
|
|
334
|
+
assert.equal(receipts.filter(r => r.admit).length, 100);
|
|
335
|
+
|
|
336
|
+
// All should have unique IDs
|
|
337
|
+
const ids = new Set(receipts.map(r => r.receipt_id));
|
|
338
|
+
assert.equal(ids.size, 100);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|