@agoric/swing-store 0.9.2-dev-bf1e645.0 → 0.9.2-dev-914bee2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/swing-store",
3
- "version": "0.9.2-dev-bf1e645.0+bf1e645",
3
+ "version": "0.9.2-dev-914bee2.0+914bee2",
4
4
  "description": "Persistent storage for SwingSet",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -21,7 +21,7 @@
21
21
  "lint:eslint": "eslint ."
22
22
  },
23
23
  "dependencies": {
24
- "@agoric/internal": "0.3.3-dev-bf1e645.0+bf1e645",
24
+ "@agoric/internal": "0.3.3-dev-914bee2.0+914bee2",
25
25
  "@endo/base64": "^1.0.7",
26
26
  "@endo/bundle-source": "^3.4.0",
27
27
  "@endo/check-bundle": "^1.0.9",
@@ -51,5 +51,5 @@
51
51
  "typeCoverage": {
52
52
  "atLeast": 78.77
53
53
  },
54
- "gitHead": "bf1e6453eee701f3e465120a03877cf44d458cc6"
54
+ "gitHead": "914bee2aa7472039ba230267d72027b940bf8a1c"
55
55
  }
@@ -219,10 +219,12 @@ export function makeTranscriptStore(
219
219
  */
220
220
  function initTranscript(vatID) {
221
221
  ensureTxn();
222
- const initialIncarnation = 0;
223
- sqlWriteSpan.run(vatID, 0, 0, initialHash, 1, initialIncarnation);
224
- const newRec = spanRec(vatID, 0, 0, initialHash, true, 0);
225
- noteExport(spanMetadataKey(newRec), JSON.stringify(newRec));
222
+ const pos = 0;
223
+ const isCurrent = 1;
224
+ const incarnation = 0;
225
+ sqlWriteSpan.run(vatID, pos, pos, initialHash, isCurrent, incarnation);
226
+ const rec = spanRec(vatID, pos, pos, initialHash, isCurrent, incarnation);
227
+ noteExport(spanMetadataKey(rec), JSON.stringify(rec));
226
228
  }
227
229
 
228
230
  const sqlGetCurrentSpanBounds = db.prepare(`
@@ -255,32 +257,27 @@ export function makeTranscriptStore(
255
257
  WHERE vatID = ? AND position >= ? AND position < ?
256
258
  `);
257
259
 
258
- function doSpanRollover(vatID, isNewIncarnation) {
260
+ /**
261
+ * Finalize a span, setting isCurrent to null, marking the resulting record
262
+ * for export, and effecting disk archival and database cleanup as
263
+ * configured.
264
+ * Note that creation of a new DB row and removal/replacement of the
265
+ * transcript.${vatID}.current export record are responsibility of the caller.
266
+ *
267
+ * @param {string} vatID
268
+ * @param {ReturnType<getCurrentSpanBounds>} bounds
269
+ */
270
+ function closeSpan(vatID, bounds) {
259
271
  ensureTxn();
260
- const { hash, startPos, endPos, incarnation } = getCurrentSpanBounds(vatID);
272
+ const { startPos, endPos, hash, incarnation } = bounds;
261
273
  const rec = spanRec(vatID, startPos, endPos, hash, false, incarnation);
262
274
 
263
- // add a new record for the now-old span
275
+ // add a new export record for the now-old span
264
276
  noteExport(spanMetadataKey(rec), JSON.stringify(rec));
265
277
 
266
- // and change its DB row to isCurrent=0
278
+ // and change its DB row to isCurrent=null
267
279
  sqlEndCurrentSpan.run(vatID);
268
280
 
269
- // create a new (empty) row, with isCurrent=1
270
- const incarnationToUse = isNewIncarnation ? incarnation + 1 : incarnation;
271
- sqlWriteSpan.run(vatID, endPos, endPos, initialHash, 1, incarnationToUse);
272
-
273
- // overwrite the transcript.${vatID}.current record with new span
274
- const newRec = spanRec(
275
- vatID,
276
- endPos,
277
- endPos,
278
- initialHash,
279
- true,
280
- incarnationToUse,
281
- );
282
- noteExport(spanMetadataKey(newRec), JSON.stringify(newRec));
283
-
284
281
  if (!keepTranscripts) {
285
282
  // Delete items of the previously-current span.
286
283
  // There may still be items associated with even older spans, but we leave
@@ -290,7 +287,32 @@ export function makeTranscriptStore(
290
287
  // that doesn't include them.
291
288
  sqlDeleteOldItems.run(vatID, startPos, endPos);
292
289
  }
293
- return incarnationToUse;
290
+ }
291
+
292
+ function doSpanRollover(vatID, isNewIncarnation) {
293
+ ensureTxn();
294
+ const bounds = getCurrentSpanBounds(vatID);
295
+ const { endPos, incarnation } = bounds;
296
+
297
+ // deal with the now-old span
298
+ closeSpan(vatID, bounds);
299
+
300
+ // create a new (empty) DB row, with isCurrent=1
301
+ const newSpanIncarnation = isNewIncarnation ? incarnation + 1 : incarnation;
302
+ sqlWriteSpan.run(vatID, endPos, endPos, initialHash, 1, newSpanIncarnation);
303
+
304
+ // overwrite the transcript.${vatID}.current record with new span
305
+ const rec = spanRec(
306
+ vatID,
307
+ endPos,
308
+ endPos,
309
+ initialHash,
310
+ true,
311
+ newSpanIncarnation,
312
+ );
313
+ noteExport(spanMetadataKey(rec), JSON.stringify(rec));
314
+
315
+ return newSpanIncarnation;
294
316
  }
295
317
 
296
318
  /**
@@ -360,28 +382,21 @@ export function makeTranscriptStore(
360
382
  `);
361
383
 
362
384
  /**
363
- * Prepare for vat deletion by marking the isCurrent span as not
364
- * current. Idempotent.
385
+ * Prepare for vat deletion by marking the isCurrent=1 span as not current.
386
+ * Idempotent.
365
387
  *
366
388
  * @param {string} vatID The vat being terminated/deleted.
367
389
  */
368
390
  function stopUsingTranscript(vatID) {
369
391
  ensureTxn();
370
- // this transforms the current span into a (short) historical one
371
- // (basically doSpanRollover without adding replacement data)
372
392
  const bounds = sqlGetCurrentSpanBounds.get(vatID);
373
- if (bounds) {
374
- // add a new record for the now-old span
375
- const { startPos, endPos, hash, incarnation } = bounds;
376
- const rec = spanRec(vatID, startPos, endPos, hash, false, incarnation);
377
- noteExport(spanMetadataKey(rec), JSON.stringify(rec));
393
+ if (!bounds) return;
378
394
 
379
- // and change its DB row to isCurrent=0
380
- sqlEndCurrentSpan.run(vatID);
395
+ // deal with the now-old span
396
+ closeSpan(vatID, bounds);
381
397
 
382
- // remove the transcript.${vatID}.current record
383
- noteExport(spanMetadataKey({ vatID, isCurrent: true }), undefined);
384
- }
398
+ // remove the transcript.${vatID}.current record
399
+ noteExport(spanMetadataKey({ vatID, isCurrent: true }), undefined);
385
400
  }
386
401
 
387
402
  /**
@@ -542,7 +557,7 @@ export function makeTranscriptStore(
542
557
  }
543
558
  }
544
559
  } else if (artifactMode === 'archival') {
545
- // every span for all vatIDs that have an isCurrent span (to
560
+ // every span for all vatIDs that have an isCurrent=1 span (to
546
561
  // ignore terminated/partially-deleted vats)
547
562
  const vatIDs = new Set();
548
563
  for (const { vatID } of sqlGetCurrentSpanMetadata.iterate()) {
@@ -4,6 +4,7 @@ import path from 'path';
4
4
 
5
5
  import { Buffer } from 'node:buffer';
6
6
  import sqlite3 from 'better-sqlite3';
7
+ import { arrayIsLike } from '@agoric/internal/tools/ava-assertions.js';
7
8
  import { tmpDir } from './util.js';
8
9
  import { initSwingStore } from '../src/swingStore.js';
9
10
  import { makeSwingStoreExporter } from '../src/exporter.js';
@@ -50,32 +51,31 @@ test('delete snapshots with export callback', async t => {
50
51
 
51
52
  await commit();
52
53
 
53
- t.is(exportLog.length, 4);
54
- t.is(exportLog[0][0], 'snapshot.v1.10');
55
- t.is(exportLog[1][0], 'snapshot.v1.11');
56
- t.is(exportLog[2][0], 'snapshot.v1.12');
57
- t.is(exportLog[3][0], 'snapshot.v1.current');
58
54
  const hash = JSON.parse(exportLog[0][1]).hash;
55
+ arrayIsLike(t, exportLog.splice(0), [
56
+ ['snapshot.v1.10'],
57
+ ['snapshot.v1.11'],
58
+ ['snapshot.v1.12'],
59
+ ['snapshot.v1.current'],
60
+ ]);
59
61
  t.deepEqual(exportData, {
60
62
  'snapshot.v1.10': JSON.stringify({ vatID, snapPos: 10, hash, inUse: 0 }),
61
63
  'snapshot.v1.11': JSON.stringify({ vatID, snapPos: 11, hash, inUse: 0 }),
62
64
  'snapshot.v1.12': JSON.stringify({ vatID, snapPos: 12, hash, inUse: 1 }),
63
65
  'snapshot.v1.current': 'snapshot.v1.12',
64
66
  });
65
- exportLog.length = 0;
66
67
 
67
68
  // in a previous version, deleteVatSnapshots caused overlapping SQL
68
69
  // queries, and failed
69
70
  snapStore.deleteVatSnapshots(vatID);
70
71
  await commit();
71
72
 
72
- t.deepEqual(exportLog, [
73
+ t.deepEqual(exportLog.splice(0), [
73
74
  ['snapshot.v1.10', null],
74
75
  ['snapshot.v1.11', null],
75
76
  ['snapshot.v1.12', null],
76
77
  ['snapshot.v1.current', null],
77
78
  ]);
78
- exportLog.length = 0;
79
79
  t.deepEqual(exportData, {});
80
80
  });
81
81
 
@@ -104,22 +104,20 @@ test('delete transcripts with export callback', async t => {
104
104
 
105
105
  await commit();
106
106
 
107
- t.is(exportLog.length, 2);
108
- t.is(exportLog[0][0], 'transcript.v1.0');
109
- t.is(exportLog[1][0], 'transcript.v1.current');
110
- exportLog.length = 0;
107
+ arrayIsLike(t, exportLog.splice(0), [
108
+ ['transcript.v1.0'],
109
+ ['transcript.v1.current'],
110
+ ]);
111
111
 
112
112
  // in a previous version, deleteVatTranscripts caused overlapping SQL
113
113
  // queries, and failed
114
114
  transcriptStore.deleteVatTranscripts('v1');
115
115
  await commit();
116
116
 
117
- t.deepEqual(exportLog, [
117
+ t.deepEqual(exportLog.splice(0), [
118
118
  ['transcript.v1.0', null],
119
119
  ['transcript.v1.current', null],
120
120
  ]);
121
-
122
- exportLog.length = 0;
123
121
  });
124
122
 
125
123
  const getExport = async (dbDir, artifactMode) => {
@@ -145,16 +143,20 @@ const reImport = async (t, dbDir, artifactMode) => {
145
143
  return sqlite3(path.join(dbDir2, 'swingstore.sqlite'));
146
144
  };
147
145
 
148
- const compareNoHash = (t, obj1, obj2) => {
149
- const o1 = {};
150
- for (const [key, value] of Object.entries(obj1)) {
146
+ const stripHashes = exportData => {
147
+ const stripped = {};
148
+ const entries =
149
+ exportData instanceof Map
150
+ ? exportData.entries()
151
+ : Object.entries(exportData);
152
+ for (const [key, value] of entries) {
151
153
  const { hash: _, ...data } = JSON.parse(value);
152
- o1[key] = data;
154
+ stripped[key] = data;
153
155
  }
154
- return t.deepEqual(o1, obj2);
156
+ return stripped;
155
157
  };
156
158
 
157
- const setupTranscript = async t => {
159
+ const setupTranscript = async (t, keepTranscripts) => {
158
160
  const vatID = 'v1';
159
161
  const exportLog = [];
160
162
  const currentExportData = {};
@@ -166,7 +168,7 @@ const setupTranscript = async t => {
166
168
  };
167
169
  const [dbDir, cleanup] = await tmpDir('testdb');
168
170
  t.teardown(cleanup);
169
- const store = initSwingStore(dbDir, { exportCallback });
171
+ const store = initSwingStore(dbDir, { exportCallback, keepTranscripts });
170
172
  const { kernelStorage, hostStorage } = store;
171
173
  const { transcriptStore } = kernelStorage;
172
174
  const { commit } = hostStorage;
@@ -199,7 +201,11 @@ const setupTranscript = async t => {
199
201
  };
200
202
  };
201
203
 
202
- test('slow deletion of transcripts', async t => {
204
+ /**
205
+ * @param {import('ava').ExecutionContext} t
206
+ * @param {{ keepTranscripts: boolean }} config
207
+ */
208
+ const execSlowTranscriptDeletion = async (t, { keepTranscripts }) => {
203
209
  // slow transcript deletion should remove export-data as it removes
204
210
  // transcript spans and their items
205
211
 
@@ -211,27 +217,42 @@ test('slow deletion of transcripts', async t => {
211
217
  exportLog,
212
218
  currentExportData,
213
219
  vatID,
214
- } = await setupTranscript(t);
215
-
216
- t.is(exportLog.length, 4);
217
- t.is(exportLog[0][0], 'transcript.v1.0');
218
- t.is(exportLog[1][0], 'transcript.v1.2');
219
- t.is(exportLog[2][0], 'transcript.v1.4');
220
- t.is(exportLog[3][0], 'transcript.v1.current');
221
- exportLog.length = 0;
220
+ } = await setupTranscript(t, keepTranscripts);
221
+
222
+ arrayIsLike(t, exportLog.splice(0), [
223
+ ['transcript.v1.0'],
224
+ ['transcript.v1.2'],
225
+ ['transcript.v1.4'],
226
+ ['transcript.v1.current'],
227
+ ]);
222
228
  const t0 = { vatID, startPos: 0, endPos: 2, isCurrent: 0, incarnation: 0 };
223
229
  const t2 = { vatID, startPos: 2, endPos: 4, isCurrent: 0, incarnation: 0 };
224
230
  const t4 = { vatID, startPos: 4, endPos: 6, isCurrent: 0, incarnation: 1 };
225
- const tc = { vatID, startPos: 6, endPos: 8, isCurrent: 1, incarnation: 1 };
226
231
  const t6 = { vatID, startPos: 6, endPos: 8, isCurrent: 0, incarnation: 1 };
227
- compareNoHash(t, currentExportData, {
232
+ const expectedLiveExportData = {
228
233
  'transcript.v1.0': t0,
229
234
  'transcript.v1.2': t2,
230
235
  'transcript.v1.4': t4,
231
- 'transcript.v1.current': tc,
232
- });
233
-
234
- t.is(db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 8);
236
+ 'transcript.v1.current': { ...t6, isCurrent: 1 },
237
+ };
238
+ const expectedStoppedExportData = {
239
+ 'transcript.v1.0': t0,
240
+ 'transcript.v1.2': t2,
241
+ 'transcript.v1.4': t4,
242
+ 'transcript.v1.6': t6,
243
+ };
244
+ const expectedArtifactNames = [
245
+ 'transcript.v1.0.2',
246
+ 'transcript.v1.2.4',
247
+ 'transcript.v1.4.6',
248
+ 'transcript.v1.6.8',
249
+ ];
250
+
251
+ t.deepEqual(stripHashes(currentExportData), expectedLiveExportData);
252
+ t.is(
253
+ db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(),
254
+ keepTranscripts ? 8 : 2,
255
+ );
235
256
  t.is(db.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 4);
236
257
 
237
258
  // an "operational"-mode export should list all spans, but only have
@@ -239,34 +260,21 @@ test('slow deletion of transcripts', async t => {
239
260
  {
240
261
  const { exportData, artifactNames } = await getExport(dbDir, 'operational');
241
262
  t.deepEqual(currentExportData, mapToObj(exportData));
242
- compareNoHash(t, mapToObj(exportData), {
243
- 'transcript.v1.0': t0,
244
- 'transcript.v1.2': t2,
245
- 'transcript.v1.4': t4,
246
- 'transcript.v1.current': tc,
247
- });
248
- t.deepEqual(artifactNames, ['transcript.v1.6.8']);
263
+ t.deepEqual(stripHashes(exportData), expectedLiveExportData);
264
+ t.deepEqual(artifactNames, expectedArtifactNames.slice(-1));
249
265
  const db2 = await reImport(t, dbDir, 'operational');
250
266
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 2);
251
267
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 4);
252
268
  }
253
269
 
254
- // an "archival"-mode export should list all four spans, with
255
- // artifacts for each
256
- {
270
+ // an "archival"-mode export should list all four spans with
271
+ // artifacts for each, but is only available with keepTranscripts=true
272
+ if (!keepTranscripts) {
273
+ await t.throwsAsync(getExport(dbDir, 'archival'));
274
+ } else {
257
275
  const { exportData, artifactNames } = await getExport(dbDir, 'archival');
258
- compareNoHash(t, mapToObj(exportData), {
259
- 'transcript.v1.0': t0,
260
- 'transcript.v1.2': t2,
261
- 'transcript.v1.4': t4,
262
- 'transcript.v1.current': tc,
263
- });
264
- t.deepEqual(artifactNames, [
265
- 'transcript.v1.0.2',
266
- 'transcript.v1.2.4',
267
- 'transcript.v1.4.6',
268
- 'transcript.v1.6.8',
269
- ]);
276
+ t.deepEqual(stripHashes(exportData), expectedLiveExportData);
277
+ t.deepEqual(artifactNames, expectedArtifactNames);
270
278
  const db2 = await reImport(t, dbDir, 'archival');
271
279
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 8);
272
280
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 4);
@@ -278,58 +286,49 @@ test('slow deletion of transcripts', async t => {
278
286
  {
279
287
  transcriptStore.stopUsingTranscript(vatID);
280
288
  await commit();
281
- compareNoHash(t, currentExportData, {
282
- 'transcript.v1.0': t0,
283
- 'transcript.v1.2': t2,
284
- 'transcript.v1.4': t4,
285
- 'transcript.v1.6': t6,
286
- });
289
+ t.deepEqual(stripHashes(currentExportData), expectedStoppedExportData);
287
290
  exportLog.length = 0;
288
291
  // stopUsingTranscript is idempotent
289
292
  transcriptStore.stopUsingTranscript(vatID);
290
293
  await commit();
291
- t.is(exportLog.length, 0);
294
+ t.deepEqual(exportLog, []);
292
295
  }
293
296
 
294
297
  // All exports (debug and non-debug) in this "terminated but not
295
298
  // deleted" state will still have the export-data keys. Only
296
- // debug-mode will have artifacts.
299
+ // debug-mode will have artifacts (and only with keepTranscripts=true).
297
300
  for (const mode of ['operational', 'replay', 'archival', 'debug']) {
298
301
  const { exportData, artifactNames } = await getExport(dbDir, mode);
299
- compareNoHash(t, mapToObj(exportData), {
300
- 'transcript.v1.0': t0,
301
- 'transcript.v1.2': t2,
302
- 'transcript.v1.4': t4,
303
- 'transcript.v1.6': t6,
304
- });
305
- if (mode === 'debug') {
306
- t.deepEqual(artifactNames, [
307
- 'transcript.v1.0.2',
308
- 'transcript.v1.2.4',
309
- 'transcript.v1.4.6',
310
- 'transcript.v1.6.8',
311
- ]);
312
- } else {
313
- t.deepEqual(artifactNames, []);
314
- }
302
+ t.deepEqual(
303
+ stripHashes(exportData),
304
+ expectedStoppedExportData,
305
+ `${mode} stopped-vat export data`,
306
+ );
307
+ t.deepEqual(
308
+ artifactNames,
309
+ mode === 'debug' && keepTranscripts ? expectedArtifactNames : [],
310
+ `${mode} stopped-vat export artifacts`,
311
+ );
315
312
  const db2 = await reImport(t, dbDir, 'operational');
316
313
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
317
314
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 4);
318
315
  }
319
316
 
320
317
  // first deletion
318
+ const expectedTruncatedExportData1 = Object.fromEntries(
319
+ Object.entries(expectedStoppedExportData).slice(0, -1),
320
+ );
321
321
  {
322
322
  // budget=1 will let it delete one span, the last one
323
323
  const dc = transcriptStore.deleteVatTranscripts(vatID, 1);
324
324
  t.false(dc.done);
325
325
  t.is(dc.cleanups, 1);
326
326
  await commit();
327
- compareNoHash(t, currentExportData, {
328
- 'transcript.v1.0': t0,
329
- 'transcript.v1.2': t2,
330
- 'transcript.v1.4': t4,
331
- });
332
- t.is(db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 6);
327
+ t.deepEqual(stripHashes(currentExportData), expectedTruncatedExportData1);
328
+ t.is(
329
+ db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(),
330
+ keepTranscripts ? 6 : 0,
331
+ );
333
332
  t.is(db.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 3);
334
333
  }
335
334
 
@@ -341,50 +340,53 @@ test('slow deletion of transcripts', async t => {
341
340
 
342
341
  for (const mode of ['operational', 'replay', 'archival', 'debug']) {
343
342
  const { exportData, artifactNames } = await getExport(dbDir, mode);
344
- compareNoHash(t, mapToObj(exportData), {
345
- 'transcript.v1.0': t0,
346
- 'transcript.v1.2': t2,
347
- 'transcript.v1.4': t4,
348
- });
349
- if (mode === 'debug') {
350
- t.deepEqual(artifactNames, [
351
- 'transcript.v1.0.2',
352
- 'transcript.v1.2.4',
353
- 'transcript.v1.4.6',
354
- ]);
355
- } else {
356
- t.deepEqual(artifactNames, []);
357
- }
343
+ t.deepEqual(
344
+ stripHashes(exportData),
345
+ expectedTruncatedExportData1,
346
+ `${mode} first-deletion export data`,
347
+ );
348
+ t.deepEqual(
349
+ artifactNames,
350
+ mode === 'debug' && keepTranscripts
351
+ ? expectedArtifactNames.slice(0, -1)
352
+ : [],
353
+ `${mode} first-deletion export artifacts`,
354
+ );
358
355
  const db2 = await reImport(t, dbDir, 'operational');
359
356
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
360
357
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 3);
361
358
  }
362
359
 
363
360
  // second deletion
361
+ const expectedTruncatedExportData2 = Object.fromEntries(
362
+ Object.entries(expectedStoppedExportData).slice(0, -2),
363
+ );
364
364
  {
365
365
  const dc = transcriptStore.deleteVatTranscripts(vatID, 1);
366
366
  t.false(dc.done);
367
367
  t.is(dc.cleanups, 1);
368
368
  await commit();
369
- compareNoHash(t, currentExportData, {
370
- 'transcript.v1.0': t0,
371
- 'transcript.v1.2': t2,
372
- });
373
- t.is(db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 4);
369
+ t.deepEqual(stripHashes(currentExportData), expectedTruncatedExportData2);
370
+ t.is(
371
+ db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(),
372
+ keepTranscripts ? 4 : 0,
373
+ );
374
374
  t.is(db.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 2);
375
375
  }
376
-
377
376
  for (const mode of ['operational', 'replay', 'archival', 'debug']) {
378
377
  const { exportData, artifactNames } = await getExport(dbDir, mode);
379
- compareNoHash(t, mapToObj(exportData), {
380
- 'transcript.v1.0': t0,
381
- 'transcript.v1.2': t2,
382
- });
383
- if (mode === 'debug') {
384
- t.deepEqual(artifactNames, ['transcript.v1.0.2', 'transcript.v1.2.4']);
385
- } else {
386
- t.deepEqual(artifactNames, []);
387
- }
378
+ t.deepEqual(
379
+ stripHashes(exportData),
380
+ expectedTruncatedExportData2,
381
+ `${mode} second-deletion export data`,
382
+ );
383
+ t.deepEqual(
384
+ artifactNames,
385
+ mode === 'debug' && keepTranscripts
386
+ ? expectedArtifactNames.slice(0, -2)
387
+ : [],
388
+ `${mode} second-deletion export artifacts`,
389
+ );
388
390
  const db2 = await reImport(t, dbDir, 'operational');
389
391
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
390
392
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 2);
@@ -396,15 +398,18 @@ test('slow deletion of transcripts', async t => {
396
398
  t.true(dc.done);
397
399
  t.is(dc.cleanups, 2);
398
400
  await commit();
399
- compareNoHash(t, currentExportData, {});
401
+ t.deepEqual(stripHashes(currentExportData), {});
400
402
  t.is(db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
401
403
  t.is(db.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 0);
402
404
  }
403
-
404
405
  for (const mode of ['operational', 'replay', 'archival', 'debug']) {
405
406
  const { exportData, artifactNames } = await getExport(dbDir, mode);
406
- compareNoHash(t, mapToObj(exportData), {});
407
- t.deepEqual(artifactNames, []);
407
+ t.deepEqual(
408
+ stripHashes(exportData),
409
+ {},
410
+ `${mode} final-deletion export data`,
411
+ );
412
+ t.deepEqual(artifactNames, [], `${mode} final-deletion export artifacts`);
408
413
  const db2 = await reImport(t, dbDir, 'operational');
409
414
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
410
415
  t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 0);
@@ -417,9 +422,18 @@ test('slow deletion of transcripts', async t => {
417
422
  t.true(dc.done);
418
423
  t.is(dc.cleanups, 0);
419
424
  await commit();
420
- t.is(exportLog.length, 0);
425
+ t.deepEqual(exportLog, []);
421
426
  }
427
+ };
428
+ const testSlowTranscriptDeletion = test.macro({
429
+ title(extra = '', { keepTranscripts }) {
430
+ const detail = keepTranscripts ? 'with retention' : 'without retention';
431
+ return `slow deletion of transcripts ${detail}${extra.replace(/.$/, ' $&')}`;
432
+ },
433
+ exec: execSlowTranscriptDeletion,
422
434
  });
435
+ test(testSlowTranscriptDeletion, { keepTranscripts: true });
436
+ test(testSlowTranscriptDeletion, { keepTranscripts: false });
423
437
 
424
438
  test('slow deletion without stopUsingTranscript', async t => {
425
439
  // slow deletion should work even without stopUsingTranscript
@@ -438,7 +452,7 @@ test('slow deletion without stopUsingTranscript', async t => {
438
452
  const t0 = { vatID, startPos: 0, endPos: 2, isCurrent: 0, incarnation: 0 };
439
453
  const t2 = { vatID, startPos: 2, endPos: 4, isCurrent: 0, incarnation: 0 };
440
454
  const t4 = { vatID, startPos: 4, endPos: 6, isCurrent: 0, incarnation: 1 };
441
- compareNoHash(t, currentExportData, {
455
+ t.deepEqual(stripHashes(currentExportData), {
442
456
  'transcript.v1.0': t0,
443
457
  'transcript.v1.2': t2,
444
458
  'transcript.v1.4': t4,
@@ -556,7 +570,7 @@ test('slow deletion of snapshots', async t => {
556
570
  // stopUsingLastSnapshot is idempotent
557
571
  snapStore.stopUsingLastSnapshot(vatID);
558
572
  await commit();
559
- t.is(exportLog.length, 0);
573
+ t.deepEqual(exportLog, []);
560
574
  }
561
575
 
562
576
  // first deletion