@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,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for KGC Runtime Freeze-Restore functionality
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* - Freeze and verify operations
|
|
6
|
+
* - Reconstruct from snapshot
|
|
7
|
+
* - Hash consistency
|
|
8
|
+
* - Multi-snapshot handling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
import { promises as fs } from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { dirname } from 'path';
|
|
16
|
+
import {
|
|
17
|
+
freezeUniverse,
|
|
18
|
+
verifyFreeze,
|
|
19
|
+
reconstructTo,
|
|
20
|
+
getSnapshotList,
|
|
21
|
+
} from '../src/freeze-restore.mjs';
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
|
|
26
|
+
// Test snapshot directory
|
|
27
|
+
const TEST_SNAPSHOT_DIR = path.join(__dirname, '../var/kgc/snapshots-test');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Clean test snapshot directory
|
|
31
|
+
*/
|
|
32
|
+
async function cleanTestSnapshots() {
|
|
33
|
+
try {
|
|
34
|
+
await fs.rm(TEST_SNAPSHOT_DIR, { recursive: true, force: true });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// Ignore if doesn't exist
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('KGC Runtime Freeze-Restore', () => {
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
await cleanTestSnapshots();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await cleanTestSnapshots();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('freezeUniverse', () => {
|
|
50
|
+
it('should freeze simple universe state', async () => {
|
|
51
|
+
const universe = {
|
|
52
|
+
entities: ['entity1', 'entity2'],
|
|
53
|
+
count: 2,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
57
|
+
|
|
58
|
+
expect(manifest).toBeDefined();
|
|
59
|
+
expect(manifest.timestamp_ns).toBeDefined();
|
|
60
|
+
expect(manifest.o_hash).toBeDefined();
|
|
61
|
+
expect(manifest.o_hash).toHaveLength(64); // BLAKE3 produces 32-byte (64-char hex) hash
|
|
62
|
+
expect(manifest.file_count).toBe(1);
|
|
63
|
+
expect(manifest.total_bytes).toBeGreaterThan(0);
|
|
64
|
+
expect(manifest.created_at).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should freeze empty universe', async () => {
|
|
68
|
+
const universe = {};
|
|
69
|
+
|
|
70
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
71
|
+
|
|
72
|
+
expect(manifest).toBeDefined();
|
|
73
|
+
expect(manifest.o_hash).toBeDefined();
|
|
74
|
+
expect(manifest.file_count).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should freeze universe with BigInt timestamp', async () => {
|
|
78
|
+
const universe = {
|
|
79
|
+
data: 'test',
|
|
80
|
+
timestamp: 1234567890123456789n,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
84
|
+
|
|
85
|
+
expect(manifest).toBeDefined();
|
|
86
|
+
expect(manifest.timestamp_ns).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should produce deterministic hash for same state', async () => {
|
|
90
|
+
const universe = {
|
|
91
|
+
entities: ['a', 'b', 'c'],
|
|
92
|
+
count: 3,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const manifest1 = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
96
|
+
|
|
97
|
+
// Wait a bit to ensure different timestamp
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
99
|
+
|
|
100
|
+
const manifest2 = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
101
|
+
|
|
102
|
+
// Different timestamps but same hash for same content
|
|
103
|
+
expect(manifest1.timestamp_ns).not.toBe(manifest2.timestamp_ns);
|
|
104
|
+
expect(manifest1.o_hash).toBe(manifest2.o_hash);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should produce different hash for different state', async () => {
|
|
108
|
+
const universe1 = { data: 'version1' };
|
|
109
|
+
const universe2 = { data: 'version2' };
|
|
110
|
+
|
|
111
|
+
const manifest1 = await freezeUniverse(universe1, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
112
|
+
const manifest2 = await freezeUniverse(universe2, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
113
|
+
|
|
114
|
+
expect(manifest1.o_hash).not.toBe(manifest2.o_hash);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should handle nested objects', async () => {
|
|
118
|
+
const universe = {
|
|
119
|
+
level1: {
|
|
120
|
+
level2: {
|
|
121
|
+
level3: {
|
|
122
|
+
data: 'deep',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
129
|
+
|
|
130
|
+
expect(manifest).toBeDefined();
|
|
131
|
+
expect(manifest.o_hash).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should throw TypeError for non-object input', async () => {
|
|
135
|
+
await expect(freezeUniverse(null, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
136
|
+
.rejects.toThrow(TypeError);
|
|
137
|
+
|
|
138
|
+
await expect(freezeUniverse('string', { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
139
|
+
.rejects.toThrow(TypeError);
|
|
140
|
+
|
|
141
|
+
await expect(freezeUniverse(123, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
142
|
+
.rejects.toThrow(TypeError);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('verifyFreeze', () => {
|
|
147
|
+
it('should verify valid snapshot', async () => {
|
|
148
|
+
const universe = { data: 'test' };
|
|
149
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
150
|
+
|
|
151
|
+
const isValid = await verifyFreeze(manifest, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
152
|
+
|
|
153
|
+
expect(isValid).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should verify snapshot by timestamp string', async () => {
|
|
157
|
+
const universe = { data: 'test' };
|
|
158
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
159
|
+
|
|
160
|
+
const isValid = await verifyFreeze(manifest.timestamp_ns, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
161
|
+
|
|
162
|
+
expect(isValid).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should detect corrupted snapshot', async () => {
|
|
166
|
+
const universe = { data: 'original' };
|
|
167
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
168
|
+
|
|
169
|
+
// Corrupt the state file
|
|
170
|
+
const statePath = path.join(TEST_SNAPSHOT_DIR, manifest.timestamp_ns, 'state.json');
|
|
171
|
+
await fs.writeFile(statePath, JSON.stringify({ data: 'corrupted' }), 'utf-8');
|
|
172
|
+
|
|
173
|
+
const isValid = await verifyFreeze(manifest, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
174
|
+
|
|
175
|
+
expect(isValid).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should throw for non-existent snapshot', async () => {
|
|
179
|
+
await expect(verifyFreeze('999999999999999999', { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
180
|
+
.rejects.toThrow('Snapshot not found');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should throw TypeError for invalid input', async () => {
|
|
184
|
+
await expect(verifyFreeze(null, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
185
|
+
.rejects.toThrow(TypeError);
|
|
186
|
+
|
|
187
|
+
await expect(verifyFreeze(123, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
188
|
+
.rejects.toThrow(TypeError);
|
|
189
|
+
|
|
190
|
+
await expect(verifyFreeze({}, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
191
|
+
.rejects.toThrow(TypeError);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('reconstructTo', () => {
|
|
196
|
+
it('should reconstruct from snapshot', async () => {
|
|
197
|
+
const universe = {
|
|
198
|
+
entities: ['e1', 'e2', 'e3'],
|
|
199
|
+
count: 3,
|
|
200
|
+
metadata: { version: 1 },
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
204
|
+
const targetTime = BigInt(manifest.timestamp_ns);
|
|
205
|
+
|
|
206
|
+
const reconstructed = await reconstructTo(targetTime, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
207
|
+
|
|
208
|
+
expect(reconstructed).toEqual(universe);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should reconstruct from string timestamp', async () => {
|
|
212
|
+
const universe = { data: 'test' };
|
|
213
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
214
|
+
|
|
215
|
+
const reconstructed = await reconstructTo(manifest.timestamp_ns, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
216
|
+
|
|
217
|
+
expect(reconstructed).toEqual(universe);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should find closest snapshot before target time', async () => {
|
|
221
|
+
const universe1 = { version: 1 };
|
|
222
|
+
const manifest1 = await freezeUniverse(universe1, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
223
|
+
|
|
224
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
225
|
+
|
|
226
|
+
const universe2 = { version: 2 };
|
|
227
|
+
const manifest2 = await freezeUniverse(universe2, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
228
|
+
|
|
229
|
+
// Target time far in the future should get latest snapshot
|
|
230
|
+
const futureTime = BigInt(manifest2.timestamp_ns) + 1000000n;
|
|
231
|
+
const reconstructed = await reconstructTo(futureTime, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
232
|
+
|
|
233
|
+
expect(reconstructed).toEqual(universe2);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should reconstruct earliest snapshot when target is far past', async () => {
|
|
237
|
+
const universe = { data: 'earliest' };
|
|
238
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
239
|
+
|
|
240
|
+
// Target time slightly after snapshot
|
|
241
|
+
const targetTime = BigInt(manifest.timestamp_ns) + 1n;
|
|
242
|
+
const reconstructed = await reconstructTo(targetTime, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
243
|
+
|
|
244
|
+
expect(reconstructed).toEqual(universe);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should handle BigInt values in reconstructed state', async () => {
|
|
248
|
+
const universe = {
|
|
249
|
+
timestamp: 9876543210123456789n,
|
|
250
|
+
value: 42n,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
254
|
+
const reconstructed = await reconstructTo(BigInt(manifest.timestamp_ns), { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
255
|
+
|
|
256
|
+
expect(reconstructed.timestamp).toBe(9876543210123456789n);
|
|
257
|
+
expect(reconstructed.value).toBe(42n);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should throw when no snapshots exist', async () => {
|
|
261
|
+
await expect(reconstructTo(123456789n, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
262
|
+
.rejects.toThrow('No snapshots available');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should throw when target time is before all snapshots', async () => {
|
|
266
|
+
const universe = { data: 'test' };
|
|
267
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
268
|
+
|
|
269
|
+
const earlyTime = BigInt(manifest.timestamp_ns) - 1000000n;
|
|
270
|
+
|
|
271
|
+
await expect(reconstructTo(earlyTime, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
272
|
+
.rejects.toThrow('No snapshot found');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should require exact match when exact option is true', async () => {
|
|
276
|
+
const universe = { data: 'test' };
|
|
277
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
278
|
+
|
|
279
|
+
const exactTime = BigInt(manifest.timestamp_ns);
|
|
280
|
+
const reconstructed = await reconstructTo(exactTime, {
|
|
281
|
+
snapshotDir: TEST_SNAPSHOT_DIR,
|
|
282
|
+
exact: true,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(reconstructed).toEqual(universe);
|
|
286
|
+
|
|
287
|
+
// Different time should fail with exact option
|
|
288
|
+
const differentTime = exactTime + 1n;
|
|
289
|
+
await expect(reconstructTo(differentTime, {
|
|
290
|
+
snapshotDir: TEST_SNAPSHOT_DIR,
|
|
291
|
+
exact: true,
|
|
292
|
+
})).rejects.toThrow('No snapshot found');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should throw TypeError for invalid time', async () => {
|
|
296
|
+
await expect(reconstructTo('not-a-number', { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
297
|
+
.rejects.toThrow();
|
|
298
|
+
|
|
299
|
+
await expect(reconstructTo(null, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
300
|
+
.rejects.toThrow(TypeError);
|
|
301
|
+
|
|
302
|
+
await expect(reconstructTo({}, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
303
|
+
.rejects.toThrow(TypeError);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should throw RangeError for negative time', async () => {
|
|
307
|
+
await expect(reconstructTo(-1n, { snapshotDir: TEST_SNAPSHOT_DIR }))
|
|
308
|
+
.rejects.toThrow(RangeError);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('getSnapshotList', () => {
|
|
313
|
+
it('should return empty array when no snapshots exist', async () => {
|
|
314
|
+
const snapshots = await getSnapshotList({ snapshotDir: TEST_SNAPSHOT_DIR });
|
|
315
|
+
|
|
316
|
+
expect(snapshots).toEqual([]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should list all snapshots sorted by time (newest first)', async () => {
|
|
320
|
+
const universe1 = { version: 1 };
|
|
321
|
+
await freezeUniverse(universe1, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
322
|
+
|
|
323
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
324
|
+
|
|
325
|
+
const universe2 = { version: 2 };
|
|
326
|
+
await freezeUniverse(universe2, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
327
|
+
|
|
328
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
329
|
+
|
|
330
|
+
const universe3 = { version: 3 };
|
|
331
|
+
await freezeUniverse(universe3, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
332
|
+
|
|
333
|
+
const snapshots = await getSnapshotList({ snapshotDir: TEST_SNAPSHOT_DIR });
|
|
334
|
+
|
|
335
|
+
expect(snapshots).toHaveLength(3);
|
|
336
|
+
|
|
337
|
+
// Verify sorted newest first
|
|
338
|
+
const time1 = BigInt(snapshots[0].manifest.timestamp_ns);
|
|
339
|
+
const time2 = BigInt(snapshots[1].manifest.timestamp_ns);
|
|
340
|
+
const time3 = BigInt(snapshots[2].manifest.timestamp_ns);
|
|
341
|
+
|
|
342
|
+
expect(time1 > time2).toBe(true);
|
|
343
|
+
expect(time2 > time3).toBe(true);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should list snapshots in ascending order when requested', async () => {
|
|
347
|
+
const universe1 = { version: 1 };
|
|
348
|
+
await freezeUniverse(universe1, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
349
|
+
|
|
350
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
351
|
+
|
|
352
|
+
const universe2 = { version: 2 };
|
|
353
|
+
await freezeUniverse(universe2, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
354
|
+
|
|
355
|
+
const snapshots = await getSnapshotList({
|
|
356
|
+
snapshotDir: TEST_SNAPSHOT_DIR,
|
|
357
|
+
ascending: true,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(snapshots).toHaveLength(2);
|
|
361
|
+
|
|
362
|
+
// Verify sorted oldest first
|
|
363
|
+
const time1 = BigInt(snapshots[0].manifest.timestamp_ns);
|
|
364
|
+
const time2 = BigInt(snapshots[1].manifest.timestamp_ns);
|
|
365
|
+
|
|
366
|
+
expect(time1 < time2).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should include manifest and path for each snapshot', async () => {
|
|
370
|
+
const universe = { data: 'test' };
|
|
371
|
+
await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
372
|
+
|
|
373
|
+
const snapshots = await getSnapshotList({ snapshotDir: TEST_SNAPSHOT_DIR });
|
|
374
|
+
|
|
375
|
+
expect(snapshots).toHaveLength(1);
|
|
376
|
+
expect(snapshots[0].manifest).toBeDefined();
|
|
377
|
+
expect(snapshots[0].manifest.timestamp_ns).toBeDefined();
|
|
378
|
+
expect(snapshots[0].manifest.o_hash).toBeDefined();
|
|
379
|
+
expect(snapshots[0].path).toBeDefined();
|
|
380
|
+
expect(snapshots[0].path).toContain(TEST_SNAPSHOT_DIR);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should skip invalid snapshots', async () => {
|
|
384
|
+
const universe = { data: 'test' };
|
|
385
|
+
await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
386
|
+
|
|
387
|
+
// Create invalid snapshot directory (no manifest)
|
|
388
|
+
const invalidPath = path.join(TEST_SNAPSHOT_DIR, 'invalid-snapshot');
|
|
389
|
+
await fs.mkdir(invalidPath, { recursive: true });
|
|
390
|
+
|
|
391
|
+
const snapshots = await getSnapshotList({ snapshotDir: TEST_SNAPSHOT_DIR });
|
|
392
|
+
|
|
393
|
+
// Should only return the valid snapshot
|
|
394
|
+
expect(snapshots).toHaveLength(1);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('Hash Consistency', () => {
|
|
399
|
+
it('should produce same hash regardless of property order', async () => {
|
|
400
|
+
const universe1 = { a: 1, b: 2, c: 3 };
|
|
401
|
+
const universe2 = { c: 3, a: 1, b: 2 };
|
|
402
|
+
|
|
403
|
+
const manifest1 = await freezeUniverse(universe1, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
404
|
+
|
|
405
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
406
|
+
|
|
407
|
+
const manifest2 = await freezeUniverse(universe2, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
408
|
+
|
|
409
|
+
expect(manifest1.o_hash).toBe(manifest2.o_hash);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should produce same hash for nested objects with different property order', async () => {
|
|
413
|
+
const universe1 = {
|
|
414
|
+
outer: { a: 1, b: { x: 10, y: 20 } },
|
|
415
|
+
};
|
|
416
|
+
const universe2 = {
|
|
417
|
+
outer: { b: { y: 20, x: 10 }, a: 1 },
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const manifest1 = await freezeUniverse(universe1, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
421
|
+
|
|
422
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
423
|
+
|
|
424
|
+
const manifest2 = await freezeUniverse(universe2, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
425
|
+
|
|
426
|
+
expect(manifest1.o_hash).toBe(manifest2.o_hash);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('Multi-Snapshot Handling', () => {
|
|
431
|
+
it('should manage multiple snapshots independently', async () => {
|
|
432
|
+
const snapshots = [];
|
|
433
|
+
|
|
434
|
+
for (let i = 0; i < 5; i++) {
|
|
435
|
+
const universe = { version: i, data: `snapshot-${i}` };
|
|
436
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
437
|
+
snapshots.push(manifest);
|
|
438
|
+
|
|
439
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const list = await getSnapshotList({ snapshotDir: TEST_SNAPSHOT_DIR });
|
|
443
|
+
expect(list).toHaveLength(5);
|
|
444
|
+
|
|
445
|
+
// Verify each snapshot independently
|
|
446
|
+
for (const manifest of snapshots) {
|
|
447
|
+
const isValid = await verifyFreeze(manifest, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
448
|
+
expect(isValid).toBe(true);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should reconstruct correct state from multiple snapshots', async () => {
|
|
453
|
+
const states = [];
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < 3; i++) {
|
|
456
|
+
const universe = { version: i };
|
|
457
|
+
const manifest = await freezeUniverse(universe, { snapshotDir: TEST_SNAPSHOT_DIR });
|
|
458
|
+
states.push({ manifest, universe });
|
|
459
|
+
|
|
460
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Reconstruct each state
|
|
464
|
+
for (const { manifest, universe } of states) {
|
|
465
|
+
const reconstructed = await reconstructTo(BigInt(manifest.timestamp_ns), {
|
|
466
|
+
snapshotDir: TEST_SNAPSHOT_DIR
|
|
467
|
+
});
|
|
468
|
+
expect(reconstructed).toEqual(universe);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
});
|