@agoric/swing-store 0.9.2-dev-2f092c3.0 → 0.9.2-dev-9d4eaad.0

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.
@@ -0,0 +1,476 @@
1
+ // @ts-check
2
+
3
+ import '@endo/init/debug.js';
4
+
5
+ import path from 'path';
6
+ import { createGunzip } from 'zlib';
7
+ import { Readable } from 'stream';
8
+ import { Buffer } from 'buffer';
9
+
10
+ import sqlite3 from 'better-sqlite3';
11
+ import test from 'ava';
12
+ import { decodeBase64 } from '@endo/base64';
13
+
14
+ import { buffer } from '../src/util.js';
15
+ import { importSwingStore, makeSwingStoreExporter } from '../src/index.js';
16
+
17
+ import { tmpDir, makeB0ID } from './util.js';
18
+
19
+ const snapshotData = 'snapshot data';
20
+ // this snapHash was computed manually
21
+ const snapHash =
22
+ 'e7dee7266896538616b630a5da40a90e007726a383e005a9c9c5dd0c2daf9329';
23
+
24
+ /** @type {import('../src/bundleStore.js').Bundle} */
25
+ const bundle0 = { moduleFormat: 'nestedEvaluate', source: '1+1' };
26
+ const bundle0ID = makeB0ID(bundle0);
27
+
28
+ function convert(orig) {
29
+ const bundles = Object.fromEntries(
30
+ Object.entries(orig.bundles).map(([bundleID, encBundle]) => {
31
+ const s = new TextDecoder().decode(decodeBase64(encBundle));
32
+ assert(bundleID.startsWith('b0-'), bundleID);
33
+ const bundle = JSON.parse(s);
34
+ return [bundleID, bundle];
35
+ }),
36
+ );
37
+ return { ...orig, bundles };
38
+ }
39
+
40
+ /**
41
+ * @typedef { import('../src/exporter').KVPair } KVPair
42
+ */
43
+
44
+ /**
45
+ * @param { Map<string, string | null> } exportData
46
+ * @param { Map<string, string> } artifacts
47
+ */
48
+ export function makeExporter(exportData, artifacts) {
49
+ return {
50
+ async *getExportData() {
51
+ for (const [key, value] of exportData.entries()) {
52
+ /** @type { KVPair } */
53
+ const pair = [key, value];
54
+ yield pair;
55
+ }
56
+ },
57
+ async *getArtifactNames() {
58
+ for (const name of artifacts.keys()) {
59
+ yield name;
60
+ }
61
+ },
62
+ async *getArtifact(name) {
63
+ const data = artifacts.get(name);
64
+ assert(data, `missing artifact ${name}`);
65
+ yield Buffer.from(data);
66
+ },
67
+ // eslint-disable-next-line no-empty-function
68
+ async close() {},
69
+ };
70
+ }
71
+
72
+ test('import empty', async t => {
73
+ const [dbDir, cleanup] = await tmpDir('testdb');
74
+ t.teardown(cleanup);
75
+ const exporter = makeExporter(new Map(), new Map());
76
+ const ss = await importSwingStore(exporter, dbDir);
77
+ ss.hostStorage.commit();
78
+ const data = convert(ss.debug.dump());
79
+ t.deepEqual(data, {
80
+ kvEntries: {},
81
+ transcripts: {},
82
+ snapshots: {},
83
+ bundles: {},
84
+ });
85
+ });
86
+
87
+ export function buildData() {
88
+ // build an export manually
89
+ const exportData = new Map();
90
+ const artifacts = new Map();
91
+
92
+ // shadow kvStore
93
+ exportData.set('kv.key1', 'value1');
94
+
95
+ // now add artifacts and metadata in pairs
96
+
97
+ artifacts.set(`bundle.${bundle0ID}`, JSON.stringify(bundle0));
98
+ exportData.set(`bundle.${bundle0ID}`, bundle0ID);
99
+
100
+ const sbase = { vatID: 'v1', hash: snapHash, inUse: 0 };
101
+ const tbase = { vatID: 'v1', startPos: 0, isCurrent: 0, incarnation: 0 };
102
+ const addTS = (key, obj) =>
103
+ exportData.set(key, JSON.stringify({ ...tbase, ...obj }));
104
+ const t0hash =
105
+ '57152efdd7fdf75c03371d2b4f1088d5bf3eae7fe643babce527ff81df38998c';
106
+ const t3hash =
107
+ '1947001e78e01bd1e773feb22b4ffc530447373b9de9274d5d5fbda3f23dbf2b';
108
+ const t6hash =
109
+ 'e6b42c6a3fb94285a93162f25a9fc0145fd4c5bb144917dc572c50ae2d02ee69';
110
+
111
+ addTS(`transcript.v1.0`, { endPos: 3, hash: t0hash });
112
+ artifacts.set(
113
+ `transcript.v1.0.3`,
114
+ 'start-worker\ndelivery1\nsave-snapshot\n',
115
+ );
116
+ exportData.set(`snapshot.v1.2`, JSON.stringify({ ...sbase, snapPos: 2 }));
117
+ artifacts.set(`snapshot.v1.2`, snapshotData);
118
+
119
+ addTS(`transcript.v1.3`, { startPos: 3, endPos: 6, hash: t3hash });
120
+ artifacts.set(
121
+ 'transcript.v1.3.6',
122
+ 'load-snapshot\ndelivery2\nsave-snapshot\n',
123
+ );
124
+ exportData.set(
125
+ `snapshot.v1.5`,
126
+ JSON.stringify({ ...sbase, snapPos: 5, inUse: 1 }),
127
+ );
128
+ artifacts.set(`snapshot.v1.5`, snapshotData);
129
+
130
+ artifacts.set('transcript.v1.6.8', 'load-snapshot\ndelivery3\n');
131
+ exportData.set(`snapshot.v1.current`, 'snapshot.v1.5');
132
+ addTS(`transcript.v1.current`, {
133
+ startPos: 6,
134
+ endPos: 8,
135
+ isCurrent: 1,
136
+ hash: t6hash,
137
+ });
138
+
139
+ return { exportData, artifacts, t0hash, t3hash, t6hash };
140
+ }
141
+
142
+ const importTest = test.macro(async (t, mode) => {
143
+ const [dbDir, cleanup] = await tmpDir('testdb');
144
+ t.teardown(cleanup);
145
+
146
+ const { exportData, artifacts, t0hash, t3hash, t6hash } = buildData();
147
+
148
+ const exporter = makeExporter(exportData, artifacts);
149
+
150
+ // now import
151
+ const includeHistorical = mode === 'historical';
152
+ const options = { includeHistorical };
153
+ const ss = await importSwingStore(exporter, dbDir, options);
154
+ ss.hostStorage.commit();
155
+ const data = convert(ss.debug.dump());
156
+
157
+ const convertTranscript = (items, startPos = 0) => {
158
+ const out = {};
159
+ let pos = startPos;
160
+ for (const item of items) {
161
+ out[pos] = item;
162
+ pos += 1;
163
+ }
164
+ return out;
165
+ };
166
+
167
+ const convertSnapshots = async allVatSnapshots => {
168
+ const out = {};
169
+ for await (const [vatID, snapshots] of Object.entries(allVatSnapshots)) {
170
+ const convertedSnapshots = [];
171
+ for await (const snapshot of snapshots) {
172
+ if (!snapshot.compressedSnapshot) {
173
+ continue;
174
+ }
175
+ const gzReader = Readable.from(snapshot.compressedSnapshot);
176
+ const unzipper = createGunzip();
177
+ const snapshotReader = gzReader.pipe(unzipper);
178
+ const uncompressedSnapshot = await buffer(snapshotReader);
179
+ const converted = { ...snapshot, uncompressedSnapshot };
180
+ delete converted.compressedSnapshot;
181
+ convertedSnapshots.push(converted);
182
+ }
183
+ out[vatID] = convertedSnapshots;
184
+ }
185
+ return out;
186
+ };
187
+
188
+ t.deepEqual(data.kvEntries, { key1: 'value1' });
189
+ let ts = [];
190
+ let tsStart = 6; // start of current span
191
+ if (mode === 'historical') {
192
+ tsStart = 0; // historical means we get all spans
193
+ ts = ts.concat(['start-worker', 'delivery1', 'save-snapshot']); // 0,1,2
194
+ ts = ts.concat(['load-snapshot', 'delivery2', 'save-snapshot']); // 3,4,5
195
+ }
196
+ ts = ts.concat(['load-snapshot', 'delivery3']); // 6,7
197
+
198
+ const expectedTranscript = convertTranscript(ts, tsStart);
199
+ t.deepEqual(data.transcripts, { v1: expectedTranscript });
200
+ const uncompressedSnapshot = Buffer.from(snapshotData);
201
+ const expectedSnapshots = [];
202
+ if (mode === 'historical') {
203
+ expectedSnapshots.push({
204
+ uncompressedSnapshot,
205
+ hash: snapHash,
206
+ inUse: 0,
207
+ snapPos: 2,
208
+ });
209
+ }
210
+ expectedSnapshots.push({
211
+ uncompressedSnapshot,
212
+ hash: snapHash,
213
+ inUse: 1,
214
+ snapPos: 5,
215
+ });
216
+ t.deepEqual(await convertSnapshots(data.snapshots), {
217
+ v1: expectedSnapshots,
218
+ });
219
+ t.deepEqual(data.bundles, { [bundle0ID]: bundle0 });
220
+
221
+ // look directly at the DB to confirm presence of metadata rows
222
+ const db = sqlite3(path.join(dbDir, 'swingstore.sqlite'));
223
+ const spanRows = [
224
+ ...db.prepare('SELECT * FROM transcriptSpans ORDER BY startPos').iterate(),
225
+ ];
226
+ t.deepEqual(
227
+ spanRows.map(sr => sr.startPos),
228
+ [0, 3, 6],
229
+ );
230
+
231
+ // and a new export should include all metadata, regardless of import mode
232
+
233
+ const reExporter = makeSwingStoreExporter(dbDir, 'current');
234
+ const reExportData = new Map();
235
+ for await (const [key, value] of reExporter.getExportData()) {
236
+ reExportData.set(key, value);
237
+ }
238
+ // console.log(reExportData);
239
+
240
+ const check = (key, expected) => {
241
+ t.true(reExportData.has(key), `missing exportData ${key}`);
242
+ let value = reExportData.get(key);
243
+ reExportData.delete(key);
244
+ if (typeof expected === 'object') {
245
+ value = JSON.parse(value);
246
+ }
247
+ t.deepEqual(value, expected);
248
+ };
249
+
250
+ check('kv.key1', 'value1');
251
+ check('snapshot.v1.2', { vatID: 'v1', snapPos: 2, inUse: 0, hash: snapHash });
252
+ check('snapshot.v1.5', { vatID: 'v1', snapPos: 5, inUse: 1, hash: snapHash });
253
+ check('snapshot.v1.current', 'snapshot.v1.5');
254
+ const base = { vatID: 'v1', incarnation: 0, isCurrent: 0 };
255
+ check('transcript.v1.0', { ...base, startPos: 0, endPos: 3, hash: t0hash });
256
+ check('transcript.v1.3', { ...base, startPos: 3, endPos: 6, hash: t3hash });
257
+ check('transcript.v1.current', {
258
+ ...base,
259
+ startPos: 6,
260
+ endPos: 8,
261
+ isCurrent: 1,
262
+ hash: t6hash,
263
+ });
264
+ check(`bundle.${bundle0ID}`, bundle0ID);
265
+
266
+ // the above list is supposed to be exhaustive
267
+ if (reExportData.size) {
268
+ console.log(reExportData);
269
+ t.fail('unexpected exportData keys');
270
+ }
271
+ });
272
+
273
+ test('import current', importTest, 'current');
274
+ test('import historical', importTest, 'historical');
275
+
276
+ test('import is missing bundle', async t => {
277
+ const [dbDir, cleanup] = await tmpDir('testdb');
278
+ t.teardown(cleanup);
279
+
280
+ const exportData = new Map();
281
+ exportData.set(`bundle.${bundle0ID}`, bundle0ID);
282
+ // but there is no artifact to match
283
+ const exporter = makeExporter(exportData, new Map());
284
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
285
+ message: /missing bundles for:/,
286
+ });
287
+ });
288
+
289
+ test('import is missing snapshot', async t => {
290
+ const [dbDir, cleanup] = await tmpDir('testdb');
291
+ t.teardown(cleanup);
292
+
293
+ const exportData = new Map();
294
+ exportData.set(
295
+ `snapshot.v1.2`,
296
+ JSON.stringify({ vatID: 'v1', hash: snapHash, inUse: 1, snapPos: 2 }),
297
+ );
298
+ // but there is no artifact to match
299
+ const exporter = makeExporter(exportData, new Map());
300
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
301
+ message: /current snapshots are pruned for vats/,
302
+ });
303
+ });
304
+
305
+ test('import is missing transcript span', async t => {
306
+ const [dbDir, cleanup] = await tmpDir('testdb');
307
+ t.teardown(cleanup);
308
+
309
+ const exportData = new Map();
310
+ const t0hash =
311
+ '57152efdd7fdf75c03371d2b4f1088d5bf3eae7fe643babce527ff81df38998c';
312
+ exportData.set(
313
+ `transcript.v1.current`,
314
+ JSON.stringify({
315
+ vatID: 'v1',
316
+ startPos: 0,
317
+ endPos: 3,
318
+ hash: t0hash,
319
+ isCurrent: 1,
320
+ incarnation: 0,
321
+ }),
322
+ );
323
+ // but there is no artifact to match
324
+ const exporter = makeExporter(exportData, new Map());
325
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
326
+ message: /incomplete current transcript/,
327
+ });
328
+ });
329
+
330
+ test('import has mismatched transcript span', async t => {
331
+ const [dbDir, cleanup] = await tmpDir('testdb');
332
+ t.teardown(cleanup);
333
+
334
+ const exportData = new Map();
335
+ const t0hash =
336
+ '57152efdd7fdf75c03371d2b4f1088d5bf3eae7fe643babce527ff81df38998c';
337
+ exportData.set(
338
+ `transcript.v1.current`,
339
+ JSON.stringify({
340
+ vatID: 'v1',
341
+ startPos: 0,
342
+ endPos: 3,
343
+ hash: t0hash,
344
+ isCurrent: 0, // mismatch
345
+ incarnation: 0,
346
+ }),
347
+ );
348
+ const exporter = makeExporter(exportData, new Map());
349
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
350
+ message: /transcript key "transcript.v1.current" mismatches metadata/,
351
+ });
352
+ });
353
+
354
+ test('import has incomplete transcript span', async t => {
355
+ const [dbDir, cleanup] = await tmpDir('testdb');
356
+ t.teardown(cleanup);
357
+
358
+ const exportData = new Map();
359
+ const artifacts = new Map();
360
+ const t0hash =
361
+ '57152efdd7fdf75c03371d2b4f1088d5bf3eae7fe643babce527ff81df38998c';
362
+ exportData.set(
363
+ `transcript.v1.current`,
364
+ JSON.stringify({
365
+ vatID: 'v1',
366
+ startPos: 0,
367
+ endPos: 4, // expect 4 items
368
+ hash: t0hash,
369
+ isCurrent: 1,
370
+ incarnation: 0,
371
+ }),
372
+ );
373
+ // but artifact only contains 3
374
+ artifacts.set(
375
+ `transcript.v1.0.4`,
376
+ 'start-worker\ndelivery1\nsave-snapshot\n',
377
+ );
378
+
379
+ const exporter = makeExporter(exportData, artifacts);
380
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
381
+ message: /artifact "transcript.v1.0.4" is not complete/,
382
+ });
383
+ });
384
+
385
+ test('import has corrupt transcript span', async t => {
386
+ const [dbDir, cleanup] = await tmpDir('testdb');
387
+ t.teardown(cleanup);
388
+
389
+ const exportData = new Map();
390
+ const artifacts = new Map();
391
+ const t0hash =
392
+ '57152efdd7fdf75c03371d2b4f1088d5bf3eae7fe643babce527ff81df38998c';
393
+ exportData.set(
394
+ `transcript.v1.current`,
395
+ JSON.stringify({
396
+ vatID: 'v1',
397
+ startPos: 0,
398
+ endPos: 3,
399
+ hash: t0hash,
400
+ isCurrent: 1,
401
+ incarnation: 0,
402
+ }),
403
+ );
404
+ artifacts.set(
405
+ `transcript.v1.0.3`,
406
+ 'start-worker\nBAD-DELIVERY1\nsave-snapshot\n',
407
+ );
408
+
409
+ const exporter = makeExporter(exportData, artifacts);
410
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
411
+ message: /artifact "transcript.v1.0.3" hash is.*metadata says/,
412
+ });
413
+ });
414
+
415
+ test('import has corrupt snapshot', async t => {
416
+ const [dbDir, cleanup] = await tmpDir('testdb');
417
+ t.teardown(cleanup);
418
+
419
+ const exportData = new Map();
420
+ const artifacts = new Map();
421
+ exportData.set(
422
+ `snapshot.v1.2`,
423
+ JSON.stringify({
424
+ vatID: 'v1',
425
+ snapPos: 2,
426
+ hash: snapHash,
427
+ inUse: 1,
428
+ }),
429
+ );
430
+ artifacts.set('snapshot.v1.2', `${snapshotData}WRONG`);
431
+
432
+ const exporter = makeExporter(exportData, artifacts);
433
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
434
+ message: /snapshot "snapshot.v1.2" hash is.*metadata says/,
435
+ });
436
+ });
437
+
438
+ test('import has corrupt bundle', async t => {
439
+ const [dbDir, cleanup] = await tmpDir('testdb');
440
+ t.teardown(cleanup);
441
+
442
+ const exportData = new Map();
443
+ const artifacts = new Map();
444
+ exportData.set(`bundle.${bundle0ID}`, bundle0ID);
445
+ const badBundle = { ...bundle0, source: 'WRONG' };
446
+ artifacts.set(`bundle.${bundle0ID}`, JSON.stringify(badBundle));
447
+
448
+ const exporter = makeExporter(exportData, artifacts);
449
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
450
+ message: /bundleID ".*" does not match bundle artifact/,
451
+ });
452
+ });
453
+
454
+ test('import has unknown metadata tag', async t => {
455
+ const [dbDir, cleanup] = await tmpDir('testdb');
456
+ t.teardown(cleanup);
457
+
458
+ const exportData = new Map();
459
+ exportData.set(`unknown.v1.current`, 'value');
460
+ const exporter = makeExporter(exportData, new Map());
461
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
462
+ message: /unknown export-data type "unknown" on import/,
463
+ });
464
+ });
465
+
466
+ test('import has unknown artifact tag', async t => {
467
+ const [dbDir, cleanup] = await tmpDir('testdb');
468
+ t.teardown(cleanup);
469
+
470
+ const artifacts = new Map();
471
+ artifacts.set('unknown.v1.current', 'value');
472
+ const exporter = makeExporter(new Map(), artifacts);
473
+ await t.throwsAsync(async () => importSwingStore(exporter, dbDir), {
474
+ message: /unknown artifact type "unknown" on import/,
475
+ });
476
+ });
@@ -0,0 +1,131 @@
1
+ // @ts-check
2
+
3
+ import '@endo/init/debug.js';
4
+
5
+ import path from 'path';
6
+ import test from 'ava';
7
+ import sqlite3 from 'better-sqlite3';
8
+
9
+ import { importSwingStore } from '../src/index.js';
10
+
11
+ import { makeExporter, buildData } from './test-import.js';
12
+ import { tmpDir } from './util.js';
13
+
14
+ test('repair metadata', async t => {
15
+ const [dbDir, cleanup] = await tmpDir('testdb');
16
+ t.teardown(cleanup);
17
+
18
+ const { exportData, artifacts } = buildData();
19
+
20
+ // simulate a swingstore broken by #8025 by importing everything,
21
+ // then manually deleting the historical metadata entries from the
22
+ // DB
23
+ const exporter = makeExporter(exportData, artifacts);
24
+ const ss = await importSwingStore(exporter, dbDir);
25
+ await ss.hostStorage.commit();
26
+
27
+ const filePath = path.join(dbDir, 'swingstore.sqlite');
28
+ const db = sqlite3(filePath);
29
+
30
+ const getTS = db.prepare(
31
+ 'SELECT startPos FROM transcriptSpans WHERE vatID = ? ORDER BY startPos',
32
+ );
33
+ getTS.pluck();
34
+ const getSS = db.prepare(
35
+ 'SELECT snapPos FROM snapshots WHERE vatID = ? ORDER BY snapPos',
36
+ );
37
+ getSS.pluck();
38
+
39
+ // assert that all the metadata is there at first
40
+ const ts1 = getTS.all('v1');
41
+ t.deepEqual(ts1, [0, 3, 6]); // three spans
42
+ const ss1 = getSS.all('v1');
43
+ t.deepEqual(ss1, [2, 5]); // two snapshots
44
+
45
+ // now clobber them to simulate #8025 (note: these auto-commit)
46
+ db.prepare('DELETE FROM transcriptSpans WHERE isCurrent IS NULL').run();
47
+ db.prepare('DELETE FROM snapshots WHERE inUSE IS NULL').run();
48
+
49
+ // confirm that we clobbered them
50
+ const ts2 = getTS.all('v1');
51
+ t.deepEqual(ts2, [6]); // only the latest
52
+ const ss2 = getSS.all('v1');
53
+ t.deepEqual(ss2, [5]);
54
+
55
+ // now fix it
56
+ await ss.hostStorage.repairMetadata(exporter);
57
+ await ss.hostStorage.commit();
58
+
59
+ // and check that the metadata is back
60
+ const ts3 = getTS.all('v1');
61
+ t.deepEqual(ts3, [0, 3, 6]); // all three again
62
+ const ss3 = getSS.all('v1');
63
+ t.deepEqual(ss3, [2, 5]);
64
+
65
+ // repair should be idempotent
66
+ await ss.hostStorage.repairMetadata(exporter);
67
+
68
+ const ts4 = getTS.all('v1');
69
+ t.deepEqual(ts4, [0, 3, 6]); // still there
70
+ const ss4 = getSS.all('v1');
71
+ t.deepEqual(ss4, [2, 5]);
72
+ });
73
+
74
+ test('repair metadata ignores kvStore entries', async t => {
75
+ const [dbDir, cleanup] = await tmpDir('testdb');
76
+ t.teardown(cleanup);
77
+
78
+ const { exportData, artifacts } = buildData();
79
+
80
+ const exporter = makeExporter(exportData, artifacts);
81
+ const ss = await importSwingStore(exporter, dbDir);
82
+ await ss.hostStorage.commit();
83
+
84
+ // perform the repair with spurious kv entries
85
+ exportData.set('kv.key2', 'value2');
86
+ await ss.hostStorage.repairMetadata(exporter);
87
+ await ss.hostStorage.commit();
88
+
89
+ // the spurious kv entry should be ignored
90
+ t.deepEqual(ss.debug.dump().kvEntries, { key1: 'value1' });
91
+ });
92
+
93
+ test('repair metadata rejects mismatched snapshot entries', async t => {
94
+ const [dbDir, cleanup] = await tmpDir('testdb');
95
+ t.teardown(cleanup);
96
+
97
+ const { exportData, artifacts } = buildData();
98
+
99
+ const exporter = makeExporter(exportData, artifacts);
100
+ const ss = await importSwingStore(exporter, dbDir);
101
+ await ss.hostStorage.commit();
102
+
103
+ // perform the repair with mismatched snapshot entry
104
+ const old = JSON.parse(exportData.get('snapshot.v1.2'));
105
+ const wrong = { ...old, hash: 'wrong' };
106
+ exportData.set('snapshot.v1.2', JSON.stringify(wrong));
107
+
108
+ await t.throwsAsync(async () => ss.hostStorage.repairMetadata(exporter), {
109
+ message: /repairSnapshotRecord metadata mismatch/,
110
+ });
111
+ });
112
+
113
+ test('repair metadata rejects mismatched transcript span', async t => {
114
+ const [dbDir, cleanup] = await tmpDir('testdb');
115
+ t.teardown(cleanup);
116
+
117
+ const { exportData, artifacts } = buildData();
118
+
119
+ const exporter = makeExporter(exportData, artifacts);
120
+ const ss = await importSwingStore(exporter, dbDir);
121
+ await ss.hostStorage.commit();
122
+
123
+ // perform the repair with mismatched transcript span entry
124
+ const old = JSON.parse(exportData.get('transcript.v1.0'));
125
+ const wrong = { ...old, hash: 'wrong' };
126
+ exportData.set('transcript.v1.0', JSON.stringify(wrong));
127
+
128
+ await t.throwsAsync(async () => ss.hostStorage.repairMetadata(exporter), {
129
+ message: /repairTranscriptSpanRecord metadata mismatch/,
130
+ });
131
+ });
package/test/util.js ADDED
@@ -0,0 +1,26 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import tmp from 'tmp';
3
+ import { createSHA256 } from '../src/hasher.js';
4
+
5
+ /**
6
+ * @param {string} [prefix]
7
+ * @returns {Promise<[string, () => void]>}
8
+ */
9
+ export const tmpDir = prefix =>
10
+ new Promise((resolve, reject) => {
11
+ tmp.dir({ unsafeCleanup: true, prefix }, (err, name, removeCallback) => {
12
+ if (err) {
13
+ reject(err);
14
+ } else {
15
+ resolve([name, removeCallback]);
16
+ }
17
+ });
18
+ });
19
+
20
+ export async function* getSnapshotStream(contents) {
21
+ yield Buffer.from(contents);
22
+ }
23
+
24
+ export function makeB0ID(bundle) {
25
+ return `b0-${createSHA256(JSON.stringify(bundle)).finish()}`;
26
+ }