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