@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,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tool Registry Tests
|
|
3
|
+
* @description Tests for ToolRegistry class functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
7
|
+
import { ToolRegistry, createRegistry } from '../src/tool-registry.mjs';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { dirname } from 'path';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
describe('ToolRegistry', () => {
|
|
17
|
+
let registry;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
registry = new ToolRegistry();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Tool Registration', () => {
|
|
24
|
+
it('should register a tool with valid manifest', () => {
|
|
25
|
+
const manifest = {
|
|
26
|
+
name: 'TestTool',
|
|
27
|
+
version: '1.0.0',
|
|
28
|
+
description: 'Test tool',
|
|
29
|
+
schema_in: z.object({ input: z.string() }),
|
|
30
|
+
schema_out: z.object({ output: z.string() }),
|
|
31
|
+
capabilities: ['test'],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
registry.registerTool(manifest);
|
|
35
|
+
const tool = registry.getTool('TestTool');
|
|
36
|
+
|
|
37
|
+
expect(tool).toBeDefined();
|
|
38
|
+
expect(tool.name).toBe('TestTool');
|
|
39
|
+
expect(tool.version).toBe('1.0.0');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should reject invalid manifest', () => {
|
|
43
|
+
const invalidManifest = {
|
|
44
|
+
name: 'Invalid',
|
|
45
|
+
// Missing required fields
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(() => registry.registerTool(invalidManifest)).toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should store multiple versions of same tool', () => {
|
|
52
|
+
const v1 = {
|
|
53
|
+
name: 'Tool',
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
schema_in: z.object({}),
|
|
56
|
+
schema_out: z.object({}),
|
|
57
|
+
capabilities: [],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const v2 = {
|
|
61
|
+
name: 'Tool',
|
|
62
|
+
version: '2.0.0',
|
|
63
|
+
schema_in: z.object({}),
|
|
64
|
+
schema_out: z.object({}),
|
|
65
|
+
capabilities: [],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
registry.registerTool(v1);
|
|
69
|
+
registry.registerTool(v2);
|
|
70
|
+
|
|
71
|
+
expect(registry.getToolVersion('Tool', '1.0.0')).toBeDefined();
|
|
72
|
+
expect(registry.getToolVersion('Tool', '2.0.0')).toBeDefined();
|
|
73
|
+
expect(registry.getTool('Tool').version).toBe('2.0.0'); // Latest
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Schema Conversion', () => {
|
|
78
|
+
it('should convert object schema definition to Zod', () => {
|
|
79
|
+
const manifest = {
|
|
80
|
+
name: 'Read',
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
schema_in: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
path: { type: 'string' },
|
|
86
|
+
encoding: { type: 'string' },
|
|
87
|
+
},
|
|
88
|
+
required: ['path'],
|
|
89
|
+
},
|
|
90
|
+
schema_out: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
content: { type: 'string' },
|
|
94
|
+
},
|
|
95
|
+
required: ['content'],
|
|
96
|
+
},
|
|
97
|
+
capabilities: ['file-read'],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
registry.registerTool(manifest);
|
|
101
|
+
const tool = registry.getTool('Read');
|
|
102
|
+
|
|
103
|
+
// Test that schemas work
|
|
104
|
+
expect(tool.schema_in.parse({ path: '/test' })).toBeDefined();
|
|
105
|
+
expect(() => tool.schema_in.parse({})).toThrow(); // Missing required field
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle nested object schemas', () => {
|
|
109
|
+
const manifest = {
|
|
110
|
+
name: 'Complex',
|
|
111
|
+
version: '1.0.0',
|
|
112
|
+
schema_in: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
config: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
timeout: { type: 'number' },
|
|
119
|
+
},
|
|
120
|
+
required: ['timeout'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ['config'],
|
|
124
|
+
},
|
|
125
|
+
schema_out: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
result: { type: 'string' },
|
|
129
|
+
},
|
|
130
|
+
required: ['result'],
|
|
131
|
+
},
|
|
132
|
+
capabilities: [],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
registry.registerTool(manifest);
|
|
136
|
+
const tool = registry.getTool('Complex');
|
|
137
|
+
|
|
138
|
+
expect(
|
|
139
|
+
tool.schema_in.parse({ config: { timeout: 5000 } }),
|
|
140
|
+
).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle array schemas', () => {
|
|
144
|
+
const manifest = {
|
|
145
|
+
name: 'ArrayTool',
|
|
146
|
+
version: '1.0.0',
|
|
147
|
+
schema_in: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {},
|
|
150
|
+
required: [],
|
|
151
|
+
},
|
|
152
|
+
schema_out: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
items: {
|
|
156
|
+
type: 'array',
|
|
157
|
+
items: { type: 'string' },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ['items'],
|
|
161
|
+
},
|
|
162
|
+
capabilities: [],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
registry.registerTool(manifest);
|
|
166
|
+
const tool = registry.getTool('ArrayTool');
|
|
167
|
+
|
|
168
|
+
expect(tool.schema_out.parse({ items: ['a', 'b'] })).toBeDefined();
|
|
169
|
+
expect(() => tool.schema_out.parse({ items: [1, 2] })).toThrow();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Tool Queries', () => {
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
registry.registerTool({
|
|
176
|
+
name: 'Read',
|
|
177
|
+
version: '1.0.0',
|
|
178
|
+
schema_in: z.object({}),
|
|
179
|
+
schema_out: z.object({}),
|
|
180
|
+
capabilities: ['file-read', 'filesystem-access'],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
registry.registerTool({
|
|
184
|
+
name: 'Write',
|
|
185
|
+
version: '1.0.0',
|
|
186
|
+
schema_in: z.object({}),
|
|
187
|
+
schema_out: z.object({}),
|
|
188
|
+
capabilities: ['file-write', 'filesystem-access'],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
registry.registerTool({
|
|
192
|
+
name: 'Bash',
|
|
193
|
+
version: '1.0.0',
|
|
194
|
+
schema_in: z.object({}),
|
|
195
|
+
schema_out: z.object({}),
|
|
196
|
+
capabilities: ['command-execution'],
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should get all tools', () => {
|
|
201
|
+
const tools = registry.getAllTools();
|
|
202
|
+
expect(tools).toHaveLength(3);
|
|
203
|
+
expect(tools.map((t) => t.name).sort()).toEqual([
|
|
204
|
+
'Bash',
|
|
205
|
+
'Read',
|
|
206
|
+
'Write',
|
|
207
|
+
]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should query tools by capability', () => {
|
|
211
|
+
const fileTools = registry.getToolsByCapability('filesystem-access');
|
|
212
|
+
expect(fileTools).toHaveLength(2);
|
|
213
|
+
expect(fileTools.map((t) => t.name).sort()).toEqual([
|
|
214
|
+
'Read',
|
|
215
|
+
'Write',
|
|
216
|
+
]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should check if tool has capability', () => {
|
|
220
|
+
expect(registry.hasCapability('Read', 'file-read')).toBe(true);
|
|
221
|
+
expect(registry.hasCapability('Read', 'file-write')).toBe(false);
|
|
222
|
+
expect(registry.hasCapability('NonExistent', 'any')).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('Output Validation', () => {
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
registry.registerTool({
|
|
229
|
+
name: 'TestTool',
|
|
230
|
+
version: '1.0.0',
|
|
231
|
+
schema_in: z.object({}),
|
|
232
|
+
schema_out: z.object({
|
|
233
|
+
result: z.string(),
|
|
234
|
+
count: z.number(),
|
|
235
|
+
}),
|
|
236
|
+
capabilities: [],
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should validate correct output', () => {
|
|
241
|
+
const output = { result: 'success', count: 42 };
|
|
242
|
+
expect(registry.validateOutput('TestTool', output)).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should reject invalid output', () => {
|
|
246
|
+
const output = { result: 'success' }; // Missing count
|
|
247
|
+
expect(registry.validateOutput('TestTool', output)).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should throw on non-existent tool', () => {
|
|
251
|
+
expect(() =>
|
|
252
|
+
registry.validateOutput('NonExistent', {}),
|
|
253
|
+
).toThrow();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('Registry Statistics', () => {
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
registry.registerTool({
|
|
260
|
+
name: 'Read',
|
|
261
|
+
version: '1.0.0',
|
|
262
|
+
schema_in: z.object({}),
|
|
263
|
+
schema_out: z.object({}),
|
|
264
|
+
capabilities: ['file-read', 'filesystem-access'],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
registry.registerTool({
|
|
268
|
+
name: 'Write',
|
|
269
|
+
version: '1.0.0',
|
|
270
|
+
schema_in: z.object({}),
|
|
271
|
+
schema_out: z.object({}),
|
|
272
|
+
capabilities: ['file-write', 'filesystem-access'],
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should return registry statistics', () => {
|
|
277
|
+
const stats = registry.getStats();
|
|
278
|
+
|
|
279
|
+
expect(stats.total_tools).toBe(2);
|
|
280
|
+
expect(stats.unique_capabilities).toBe(3);
|
|
281
|
+
expect(stats.capabilities.sort()).toEqual([
|
|
282
|
+
'file-read',
|
|
283
|
+
'file-write',
|
|
284
|
+
'filesystem-access',
|
|
285
|
+
]);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('Loading from File', () => {
|
|
290
|
+
it('should load tools from registry file', () => {
|
|
291
|
+
const registryPath = join(
|
|
292
|
+
__dirname,
|
|
293
|
+
'../../../var/kgc/tool-registry.json',
|
|
294
|
+
);
|
|
295
|
+
const fileRegistry = new ToolRegistry({ registryPath });
|
|
296
|
+
|
|
297
|
+
const tools = fileRegistry.getAllTools();
|
|
298
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
299
|
+
|
|
300
|
+
// Check built-in tools
|
|
301
|
+
const bashTool = fileRegistry.getTool('Bash');
|
|
302
|
+
expect(bashTool).toBeDefined();
|
|
303
|
+
expect(bashTool.name).toBe('Bash');
|
|
304
|
+
expect(bashTool.capabilities).toContain('command-execution');
|
|
305
|
+
|
|
306
|
+
const readTool = fileRegistry.getTool('Read');
|
|
307
|
+
expect(readTool).toBeDefined();
|
|
308
|
+
expect(readTool.capabilities).toContain('file-read');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should validate loaded schemas work correctly', () => {
|
|
312
|
+
const registryPath = join(
|
|
313
|
+
__dirname,
|
|
314
|
+
'../../../var/kgc/tool-registry.json',
|
|
315
|
+
);
|
|
316
|
+
const fileRegistry = new ToolRegistry({ registryPath });
|
|
317
|
+
|
|
318
|
+
const readTool = fileRegistry.getTool('Read');
|
|
319
|
+
|
|
320
|
+
// Should accept valid input
|
|
321
|
+
const validInput = readTool.schema_in.parse({ path: '/test.txt' });
|
|
322
|
+
expect(validInput.path).toBe('/test.txt');
|
|
323
|
+
|
|
324
|
+
// Should reject invalid input
|
|
325
|
+
expect(() => readTool.schema_in.parse({})).toThrow();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('Factory Function', () => {
|
|
330
|
+
it('should create registry with createRegistry', () => {
|
|
331
|
+
const registryPath = join(
|
|
332
|
+
__dirname,
|
|
333
|
+
'../../../var/kgc/tool-registry.json',
|
|
334
|
+
);
|
|
335
|
+
const reg = createRegistry(registryPath);
|
|
336
|
+
|
|
337
|
+
expect(reg).toBeInstanceOf(ToolRegistry);
|
|
338
|
+
expect(reg.getAllTools().length).toBeGreaterThan(0);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for Two-Phase Commit Transaction Manager
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import { TransactionManager } from '../src/transaction.mjs';
|
|
7
|
+
|
|
8
|
+
describe('TransactionManager', () => {
|
|
9
|
+
let txManager;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
txManager = new TransactionManager();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should execute two-phase commit successfully', async () => {
|
|
16
|
+
// Begin transaction
|
|
17
|
+
const tx = txManager.begin([
|
|
18
|
+
{
|
|
19
|
+
id: 'op1',
|
|
20
|
+
type: 'add_capsule',
|
|
21
|
+
data: { id: 'capsule1', content: 'test data' },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'op2',
|
|
25
|
+
type: 'add_capsule',
|
|
26
|
+
data: { id: 'capsule2', content: 'more test data' },
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
expect(tx.status).toBe('pending');
|
|
31
|
+
expect(tx.operations).toHaveLength(2);
|
|
32
|
+
|
|
33
|
+
// Phase 1: Prepare
|
|
34
|
+
const prepareResult = await txManager.prepare(tx.id);
|
|
35
|
+
|
|
36
|
+
expect(prepareResult.success).toBe(true);
|
|
37
|
+
expect(prepareResult.errors).toHaveLength(0);
|
|
38
|
+
expect(prepareResult.undoOps).toHaveLength(2);
|
|
39
|
+
|
|
40
|
+
const updatedTx = txManager.getTransaction(tx.id);
|
|
41
|
+
expect(updatedTx.status).toBe('prepared');
|
|
42
|
+
|
|
43
|
+
// Phase 2: Commit
|
|
44
|
+
const commitResult = await txManager.commit(tx.id);
|
|
45
|
+
|
|
46
|
+
expect(commitResult.success).toBe(true);
|
|
47
|
+
expect(commitResult.receipts).toHaveLength(2);
|
|
48
|
+
expect(commitResult.errors).toHaveLength(0);
|
|
49
|
+
|
|
50
|
+
const finalTx = txManager.getTransaction(tx.id);
|
|
51
|
+
expect(finalTx.status).toBe('committed');
|
|
52
|
+
|
|
53
|
+
// Verify state updated
|
|
54
|
+
const state = txManager.getState();
|
|
55
|
+
expect(state.capsules).toBeDefined();
|
|
56
|
+
expect(state.capsules['capsule1']).toBeDefined();
|
|
57
|
+
expect(state.capsules['capsule2']).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should rollback transaction on commit failure', async () => {
|
|
61
|
+
// Add a capsule first
|
|
62
|
+
const initialTx = txManager.begin([
|
|
63
|
+
{
|
|
64
|
+
id: 'initial',
|
|
65
|
+
type: 'add_capsule',
|
|
66
|
+
data: { id: 'existing', content: 'existing data' },
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
await txManager.prepare(initialTx.id);
|
|
71
|
+
await txManager.commit(initialTx.id);
|
|
72
|
+
|
|
73
|
+
// Create transaction that will fail during commit
|
|
74
|
+
const tx = txManager.begin([
|
|
75
|
+
{
|
|
76
|
+
id: 'op1',
|
|
77
|
+
type: 'add_capsule',
|
|
78
|
+
data: { id: 'new_capsule', content: 'new data' },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'op2',
|
|
82
|
+
type: 'invalid_type', // This will cause failure
|
|
83
|
+
data: {},
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const prepareResult = await txManager.prepare(tx.id);
|
|
88
|
+
expect(prepareResult.success).toBe(false);
|
|
89
|
+
|
|
90
|
+
const finalTx = txManager.getTransaction(tx.id);
|
|
91
|
+
expect(finalTx.status).toBe('aborted');
|
|
92
|
+
|
|
93
|
+
// Verify state not changed
|
|
94
|
+
const state = txManager.getState();
|
|
95
|
+
expect(state.capsules['new_capsule']).toBeUndefined();
|
|
96
|
+
expect(state.capsules['existing']).toBeDefined(); // Original still there
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should support cascading transactions with parent hash', async () => {
|
|
100
|
+
// First transaction
|
|
101
|
+
const tx1 = txManager.begin([
|
|
102
|
+
{
|
|
103
|
+
id: 'op1',
|
|
104
|
+
type: 'add_capsule',
|
|
105
|
+
data: { id: 'capsule1', content: 'first' },
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
await txManager.prepare(tx1.id);
|
|
110
|
+
const commit1 = await txManager.commit(tx1.id);
|
|
111
|
+
|
|
112
|
+
expect(commit1.success).toBe(true);
|
|
113
|
+
expect(commit1.receipts).toHaveLength(1);
|
|
114
|
+
|
|
115
|
+
const receipt1 = commit1.receipts[0];
|
|
116
|
+
const parentHash = receipt1.hash;
|
|
117
|
+
|
|
118
|
+
// Second transaction with parent hash
|
|
119
|
+
const tx2 = txManager.begin(
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
id: 'op2',
|
|
123
|
+
type: 'add_capsule',
|
|
124
|
+
data: { id: 'capsule2', content: 'second' },
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
parentHash
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(tx2.parentHash).toBe(parentHash);
|
|
131
|
+
|
|
132
|
+
await txManager.prepare(tx2.id);
|
|
133
|
+
await txManager.commit(tx2.id);
|
|
134
|
+
|
|
135
|
+
// Verify both transactions committed
|
|
136
|
+
const finalTx1 = txManager.getTransaction(tx1.id);
|
|
137
|
+
const finalTx2 = txManager.getTransaction(tx2.id);
|
|
138
|
+
|
|
139
|
+
expect(finalTx1.status).toBe('committed');
|
|
140
|
+
expect(finalTx2.status).toBe('committed');
|
|
141
|
+
expect(finalTx2.parentHash).toBe(receipt1.hash);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should handle rollback with undo operations', async () => {
|
|
145
|
+
// Add initial capsule
|
|
146
|
+
const tx1 = txManager.begin([
|
|
147
|
+
{
|
|
148
|
+
id: 'op1',
|
|
149
|
+
type: 'add_capsule',
|
|
150
|
+
data: { id: 'capsule1', content: 'original' },
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
await txManager.prepare(tx1.id);
|
|
155
|
+
await txManager.commit(tx1.id);
|
|
156
|
+
|
|
157
|
+
// Verify capsule added
|
|
158
|
+
let state = txManager.getState();
|
|
159
|
+
expect(state.capsules).toBeDefined();
|
|
160
|
+
expect(state.capsules['capsule1']).toBeDefined();
|
|
161
|
+
|
|
162
|
+
// Create transaction to remove it
|
|
163
|
+
const tx2 = txManager.begin([
|
|
164
|
+
{
|
|
165
|
+
id: 'op2',
|
|
166
|
+
type: 'remove_capsule',
|
|
167
|
+
data: { capsule_id: 'capsule1' },
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
await txManager.prepare(tx2.id);
|
|
172
|
+
await txManager.commit(tx2.id);
|
|
173
|
+
|
|
174
|
+
// Verify capsule removed
|
|
175
|
+
state = txManager.getState();
|
|
176
|
+
expect(state.capsules['capsule1']).toBeUndefined();
|
|
177
|
+
|
|
178
|
+
// Rollback the removal
|
|
179
|
+
const rollbackResult = await txManager.rollback(tx2.id);
|
|
180
|
+
|
|
181
|
+
expect(rollbackResult.success).toBe(true);
|
|
182
|
+
expect(rollbackResult.undone).toBe(1);
|
|
183
|
+
|
|
184
|
+
// Verify capsule restored
|
|
185
|
+
state = txManager.getState();
|
|
186
|
+
expect(state.capsules['capsule1']).toBeDefined();
|
|
187
|
+
expect(state.capsules['capsule1'].content).toBe('original');
|
|
188
|
+
});
|
|
189
|
+
});
|