@agoric/swingset-liveslots 0.10.3-u16.1 → 0.10.3-u17.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.
@@ -1,7 +1,7 @@
1
1
  import test from 'ava';
2
2
 
3
+ import { Fail } from '@endo/errors';
3
4
  import { Far } from '@endo/marshal';
4
- import { Fail } from '@agoric/assert';
5
5
  import { M, provideLazy as provide } from '@agoric/store';
6
6
  import { makePromiseKit } from '@endo/promise-kit';
7
7
  // Disabled to avoid circular dependencies.
@@ -84,7 +84,7 @@ const makeExoUtils = VatData => {
84
84
  };
85
85
 
86
86
  // cf. packages/SwingSet/test/vat-durable-promise-watcher.js
87
- const buildPromiseWatcherRootObject = (vatPowers, _vatParameters, baggage) => {
87
+ const buildPromiseWatcherRootObject = (vatPowers, vatParameters, baggage) => {
88
88
  const { VatData } = vatPowers;
89
89
  const { watchPromise } = VatData;
90
90
  const { prepareExo } = makeExoUtils(VatData);
@@ -93,37 +93,62 @@ const buildPromiseWatcherRootObject = (vatPowers, _vatParameters, baggage) => {
93
93
  onFulfilled: M.call(M.any(), M.string()).returns(),
94
94
  onRejected: M.call(M.any(), M.string()).returns(),
95
95
  });
96
+ const watchResolutions = new Map();
96
97
  const watcher = prepareExo(
97
98
  baggage,
99
+ // No longer ignoring, but the name is set in stone in `kvStoreDataV1`
98
100
  'DurablePromiseIgnorer',
99
101
  PromiseWatcherI,
100
102
  {
101
- onFulfilled(_value, _name) {},
102
- onRejected(_reason, _name) {},
103
+ onFulfilled(value, name) {
104
+ watchResolutions.set(name, { status: 'fulfilled', value });
105
+ },
106
+ onRejected(reason, name) {
107
+ watchResolutions.set(name, { status: 'rejected', reason });
108
+ },
103
109
  },
104
110
  );
105
111
 
106
- const localPromises = new Map();
112
+ const knownPromises = new Map();
107
113
 
108
- return Far('root', {
109
- exportPromise: () => [Promise.resolve()],
114
+ const root = Far('root', {
115
+ getPromise: name => {
116
+ const promise = knownPromises.get(name);
117
+ promise || Fail`promise doesn't exists: ${name}`;
118
+ return { promise };
119
+ },
120
+ importPromise: (name, promise) => {
121
+ !knownPromises.has(name) || Fail`promise already exists: ${name}`;
122
+ knownPromises.set(name, promise);
123
+ return `imported promise: ${name}`;
124
+ },
110
125
  createLocalPromise: (name, fulfillment, rejection) => {
111
- !localPromises.has(name) || Fail`local promise already exists: ${name}`;
126
+ !knownPromises.has(name) || Fail`promise already exists: ${name}`;
112
127
  const { promise, resolve, reject } = makePromiseKit();
113
128
  if (fulfillment !== undefined) {
114
129
  resolve(fulfillment);
115
130
  } else if (rejection !== undefined) {
116
131
  reject(rejection);
117
132
  }
118
- localPromises.set(name, promise);
133
+ knownPromises.set(name, promise);
119
134
  return `created local promise: ${name}`;
120
135
  },
121
- watchLocalPromise: name => {
122
- localPromises.has(name) || Fail`local promise not found: ${name}`;
123
- watchPromise(localPromises.get(name), watcher, name);
124
- return `watched local promise: ${name}`;
136
+ watchPromise: name => {
137
+ knownPromises.has(name) || Fail`promise not found: ${name}`;
138
+ watchPromise(knownPromises.get(name), watcher, name);
139
+ return `watched promise: ${name}`;
140
+ },
141
+ getWatchResolution: name => {
142
+ return watchResolutions.get(name);
125
143
  },
126
144
  });
145
+
146
+ const startOperations = vatParameters?.startOperations || [];
147
+ for (const [method, ...args] of startOperations) {
148
+ root[method](...args);
149
+ }
150
+
151
+ return root;
127
152
  };
128
153
  const kvStoreDataV1 = Object.entries({
129
154
  baggageID: 'o+d6/1',
@@ -176,23 +201,55 @@ const kvStoreDataV1VpidsToKeep = ['p-8'];
176
201
  const kvStoreDataV1KeysToKeep = ['vc.4.sp-8'];
177
202
 
178
203
  test('past-incarnation watched promises', async t => {
204
+ const S = 'settlement';
205
+ // Anchor promise counters upon which the other assertions depend.
206
+ const firstPImport = 9;
207
+ // cf. src/liveslots.js:initialIDCounters
208
+ const firstPExport = 5;
209
+
210
+ const startImportedP = firstPImport - 2;
211
+
212
+ const v1StartOperations = [
213
+ ['createLocalPromise', 'start orphaned'],
214
+ ['watchPromise', 'start orphaned'],
215
+ ['createLocalPromise', 'start fulfilled', S],
216
+ ['watchPromise', 'start fulfilled'],
217
+ ['importPromise', 'start imported', kslot(`p-${startImportedP}`)],
218
+ ['watchPromise', 'start imported'],
219
+ ];
179
220
  const kvStore = new Map();
180
- let { v, dispatch, dispatchMessage } = await setupTestLiveslots(
221
+ let {
222
+ v,
223
+ dispatch,
224
+ dispatchMessage: rawDispatch,
225
+ } = await setupTestLiveslots(
181
226
  t,
182
227
  buildPromiseWatcherRootObject,
183
228
  'durable-promise-watcher',
184
- { kvStore },
229
+ {
230
+ kvStore,
231
+ nextPromiseImportNumber: firstPImport,
232
+ vatParameters: { startOperations: v1StartOperations },
233
+ },
185
234
  );
186
235
  let vatLogs = v.log;
187
236
 
188
- // Anchor promise counters upon which the other assertions depend.
189
- const firstPImport = 1;
190
- // cf. src/liveslots.js:initialIDCounters
191
- const firstPExport = 5;
192
237
  let lastPImport = firstPImport - 1;
193
238
  let lastPExport = firstPExport - 1;
239
+ let dispatches = 0;
240
+ const exportedPromises = new Set();
241
+ const v1OrphanedPExports = [];
194
242
  const nextPImport = () => (lastPImport += 1);
195
243
  const nextPExport = () => (lastPExport += 1);
244
+ /** @type {typeof rawDispatch} */
245
+ const dispatchMessage = (...args) => {
246
+ dispatches += 1;
247
+ return rawDispatch(...args);
248
+ };
249
+ const recordExportedPromise = name => {
250
+ exportedPromises.add(name);
251
+ return name;
252
+ };
196
253
  // Ignore vatstore syscalls.
197
254
  const getDispatchLogs = () =>
198
255
  vatLogs.splice(0).filter(m => !m.type.startsWith('vatstore'));
@@ -208,18 +265,55 @@ test('past-incarnation watched promises', async t => {
208
265
  type: 'subscribe',
209
266
  target: vpid,
210
267
  });
211
- vatLogs.length = 0;
212
- await dispatchMessage('exportPromise');
268
+
269
+ // startVat logs
270
+ v1OrphanedPExports.push(nextPExport());
271
+ recordExportedPromise('start orphaned');
272
+ v1OrphanedPExports.push(nextPExport());
273
+ recordExportedPromise('start fulfilled');
213
274
  t.deepEqual(getDispatchLogs(), [
214
- fulfillmentMessage(`p-${nextPImport()}`, [kslot(`p+${nextPExport()}`)]),
215
- fulfillmentMessage(`p+${lastPExport}`, undefined),
275
+ subscribeMessage(`p-${startImportedP}`),
276
+ subscribeMessage(`p+${lastPExport - 1}`),
277
+ subscribeMessage(`p+${lastPExport}`),
278
+ fulfillmentMessage(`p+${lastPExport}`, S),
279
+ ]);
280
+ await dispatchMessage('createLocalPromise', 'exported', S);
281
+ t.deepEqual(getDispatchLogs(), [
282
+ fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: exported'),
283
+ ]);
284
+ await dispatchMessage('getPromise', recordExportedPromise('exported'));
285
+ t.deepEqual(getDispatchLogs(), [
286
+ fulfillmentMessage(`p-${nextPImport()}`, {
287
+ promise: kslot(`p+${nextPExport()}`),
288
+ }),
289
+ fulfillmentMessage(`p+${lastPExport}`, S),
290
+ ]);
291
+ const importedP = firstPImport - 1;
292
+ await dispatchMessage('importPromise', 'imported', kslot(`p-${importedP}`));
293
+ t.deepEqual(getDispatchLogs(), [
294
+ subscribeMessage(`p-${importedP}`),
295
+ fulfillmentMessage(`p-${nextPImport()}`, 'imported promise: imported'),
216
296
  ]);
217
-
218
- const S = 'settlement';
219
297
  await dispatchMessage('createLocalPromise', 'orphaned');
220
298
  t.deepEqual(getDispatchLogs(), [
221
299
  fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: orphaned'),
222
300
  ]);
301
+ await dispatchMessage('createLocalPromise', 'orphaned exported');
302
+ await dispatchMessage(
303
+ 'getPromise',
304
+ recordExportedPromise('orphaned exported'),
305
+ );
306
+ const orphanedExportedP = nextPExport();
307
+ v1OrphanedPExports.push(orphanedExportedP);
308
+ t.deepEqual(getDispatchLogs(), [
309
+ fulfillmentMessage(
310
+ `p-${nextPImport()}`,
311
+ 'created local promise: orphaned exported',
312
+ ),
313
+ fulfillmentMessage(`p-${nextPImport()}`, {
314
+ promise: kslot(`p+${orphanedExportedP}`),
315
+ }),
316
+ ]);
223
317
  await dispatchMessage('createLocalPromise', 'fulfilled', S);
224
318
  t.deepEqual(getDispatchLogs(), [
225
319
  fulfillmentMessage(
@@ -233,68 +327,185 @@ test('past-incarnation watched promises', async t => {
233
327
  ]);
234
328
  t.is(
235
329
  lastPImport - firstPImport + 1,
236
- 4,
237
- 'imported 4 promises (1 per dispatch)',
330
+ dispatches,
331
+ `imported ${dispatches} promises (1 per dispatch)`,
332
+ );
333
+ t.is(
334
+ lastPExport - firstPExport + 1,
335
+ exportedPromises.size,
336
+ `exported ${exportedPromises.size} promises: ${[...exportedPromises].join(', ')}`,
238
337
  );
239
- t.is(lastPExport - firstPExport + 1, 1, 'exported 1 promise: first');
240
338
 
241
- await dispatchMessage('watchLocalPromise', 'orphaned');
339
+ await dispatchMessage('watchPromise', recordExportedPromise('orphaned'));
340
+ v1OrphanedPExports.push(nextPExport());
242
341
  t.deepEqual(getDispatchLogs(), [
243
- subscribeMessage(`p+${nextPExport()}`),
244
- fulfillmentMessage(`p-${nextPImport()}`, 'watched local promise: orphaned'),
342
+ subscribeMessage(`p+${lastPExport}`),
343
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: orphaned'),
245
344
  ]);
246
- await dispatchMessage('watchLocalPromise', 'fulfilled');
345
+ await dispatchMessage('watchPromise', recordExportedPromise('fulfilled'));
247
346
  t.deepEqual(getDispatchLogs(), [
248
347
  subscribeMessage(`p+${nextPExport()}`),
249
- fulfillmentMessage(
250
- `p-${nextPImport()}`,
251
- 'watched local promise: fulfilled',
252
- ),
348
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: fulfilled'),
253
349
  fulfillmentMessage(`p+${lastPExport}`, S),
254
350
  ]);
255
- await dispatchMessage('watchLocalPromise', 'rejected');
351
+ await dispatchMessage('watchPromise', recordExportedPromise('rejected'));
256
352
  t.deepEqual(getDispatchLogs(), [
257
353
  subscribeMessage(`p+${nextPExport()}`),
258
- fulfillmentMessage(`p-${nextPImport()}`, 'watched local promise: rejected'),
354
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: rejected'),
259
355
  rejectionMessage(`p+${lastPExport}`, S),
260
356
  ]);
357
+ await dispatchMessage('watchPromise', 'imported');
358
+ t.deepEqual(getDispatchLogs(), [
359
+ // no subscribe, we already did at import
360
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: imported'),
361
+ ]);
362
+ await dispatchMessage('getWatchResolution', 'fulfilled');
363
+ t.deepEqual(getDispatchLogs(), [
364
+ fulfillmentMessage(`p-${nextPImport()}`, {
365
+ status: 'fulfilled',
366
+ value: S,
367
+ }),
368
+ ]);
369
+ await dispatchMessage('getWatchResolution', 'rejected');
370
+ t.deepEqual(getDispatchLogs(), [
371
+ fulfillmentMessage(`p-${nextPImport()}`, {
372
+ status: 'rejected',
373
+ reason: S,
374
+ }),
375
+ ]);
376
+ await dispatchMessage('getWatchResolution', 'start fulfilled');
377
+ t.deepEqual(getDispatchLogs(), [
378
+ fulfillmentMessage(`p-${nextPImport()}`, {
379
+ status: 'fulfilled',
380
+ value: S,
381
+ }),
382
+ ]);
383
+
261
384
  t.is(
262
385
  lastPImport - firstPImport + 1,
263
- 7,
264
- 'imported 7 promises (1 per dispatch)',
386
+ dispatches,
387
+ `imported ${dispatches} promises (1 per dispatch)`,
265
388
  );
266
389
  t.is(
267
390
  lastPExport - firstPExport + 1,
268
- 4,
269
- 'exported 4 promises: first, orphaned, fulfilled, rejected',
391
+ exportedPromises.size,
392
+ `exported ${exportedPromises.size} promises: ${[...exportedPromises].join(', ')}`,
270
393
  );
271
394
 
272
395
  // Simulate upgrade by starting from the non-empty kvStore.
273
396
  // t.log(Object.fromEntries([...kvStore.entries()].sort(compareEntriesByKey)));
274
397
  const clonedStore = new Map(kvStore);
275
- ({ v, dispatch, dispatchMessage } = await setupTestLiveslots(
398
+ const startImported2P = firstPImport - 3;
399
+ const v2StartOperations = [
400
+ ['importPromise', 'start imported 2', kslot(`p-${startImported2P}`)], // import of new promise
401
+ ['importPromise', 'imported', kslot(`p-${importedP}`)], // import previously imported and watched promise
402
+ ['importPromise', 'orphaned exported', kslot(`p+${orphanedExportedP}`)], // import previously exported but unwatched promise
403
+ ['watchPromise', 'orphaned exported'],
404
+ ];
405
+ ({
406
+ v,
407
+ dispatch,
408
+ dispatchMessage: rawDispatch,
409
+ } = await setupTestLiveslots(
276
410
  t,
277
411
  buildPromiseWatcherRootObject,
278
412
  'durable-promise-watcher-v2',
279
- { kvStore: clonedStore, nextPromiseImportNumber: lastPImport + 1 },
413
+ {
414
+ kvStore: clonedStore,
415
+ nextPromiseImportNumber: lastPImport + 1,
416
+ vatParameters: { startOperations: v2StartOperations },
417
+ },
280
418
  ));
281
419
  vatLogs = v.log;
282
420
 
421
+ // startVat logs
422
+ t.deepEqual(getDispatchLogs(), [
423
+ subscribeMessage(`p-${startImported2P}`),
424
+ subscribeMessage(`p-${importedP}`),
425
+ subscribeMessage(`p+${orphanedExportedP}`),
426
+ ]);
283
427
  // Simulate kernel rejection of promises orphaned by termination/upgrade of their decider vat.
284
428
  const expectedDeletions = [...clonedStore.entries()].filter(entry =>
285
429
  entry[1].includes('orphaned'),
286
430
  );
287
431
  t.true(expectedDeletions.length >= 1);
288
- await dispatch(
289
- makeReject(`p+${firstPExport + 1}`, kser('tomorrow never came')),
290
- );
432
+ for (const orphanedPExport of v1OrphanedPExports) {
433
+ await dispatch(
434
+ makeReject(`p+${orphanedPExport}`, kser('tomorrow never came')),
435
+ );
436
+ }
437
+ await dispatchMessage('getWatchResolution', 'orphaned');
438
+ t.deepEqual(getDispatchLogs(), [
439
+ fulfillmentMessage(`p-${nextPImport()}`, {
440
+ status: 'rejected',
441
+ reason: 'tomorrow never came',
442
+ }),
443
+ ]);
444
+ await dispatchMessage('getWatchResolution', 'start orphaned');
445
+ t.deepEqual(getDispatchLogs(), [
446
+ fulfillmentMessage(`p-${nextPImport()}`, {
447
+ status: 'rejected',
448
+ reason: 'tomorrow never came',
449
+ }),
450
+ ]);
451
+ await dispatchMessage('getWatchResolution', 'orphaned exported');
452
+ t.deepEqual(getDispatchLogs(), [
453
+ fulfillmentMessage(`p-${nextPImport()}`, {
454
+ status: 'rejected',
455
+ reason: 'tomorrow never came',
456
+ }),
457
+ ]);
291
458
  for (const [key, value] of expectedDeletions) {
292
459
  t.false(clonedStore.has(key), `entry should be removed: ${key}: ${value}`);
293
460
  }
461
+ // Simulate resolution of imported promises watched in previous incarnation
462
+ await dispatch(makeResolve(`p-${importedP}`, kser(undefined)));
463
+ await dispatchMessage('getWatchResolution', 'imported');
464
+ t.deepEqual(getDispatchLogs(), [
465
+ fulfillmentMessage(`p-${nextPImport()}`, {
466
+ status: 'fulfilled',
467
+ value: undefined,
468
+ }),
469
+ ]);
470
+ await dispatch(makeResolve(`p-${startImportedP}`, kser(undefined)));
471
+ await dispatchMessage('getWatchResolution', 'start imported');
472
+ t.deepEqual(getDispatchLogs(), [
473
+ fulfillmentMessage(`p-${nextPImport()}`, {
474
+ status: 'fulfilled',
475
+ value: undefined,
476
+ }),
477
+ ]);
478
+ await dispatch(makeResolve(`p-${startImported2P}`, kser(undefined)));
479
+ await dispatchMessage('getWatchResolution', 'start imported 2');
480
+ t.deepEqual(getDispatchLogs(), [
481
+ fulfillmentMessage(`p-${nextPImport()}`, undefined),
482
+ ]);
483
+ // simulate resolution of imported promise watched after resolution
484
+ await dispatchMessage('watchPromise', 'start imported 2');
485
+ t.deepEqual(getDispatchLogs(), [
486
+ // Promise was previously resolved, so it is re-exported
487
+ subscribeMessage(`p+${nextPExport()}`),
488
+ fulfillmentMessage(
489
+ `p-${nextPImport()}`,
490
+ 'watched promise: start imported 2',
491
+ ),
492
+ fulfillmentMessage(`p+${lastPExport}`, undefined),
493
+ ]);
494
+ await dispatchMessage('getWatchResolution', 'start imported 2');
495
+ t.deepEqual(getDispatchLogs(), [
496
+ fulfillmentMessage(`p-${nextPImport()}`, {
497
+ status: 'fulfilled',
498
+ value: undefined,
499
+ }),
500
+ ]);
294
501
 
295
502
  // Verify that the data is still in loadable condition.
296
503
  const finalClonedStore = new Map(clonedStore);
297
- ({ v, dispatch, dispatchMessage } = await setupTestLiveslots(
504
+ ({
505
+ v,
506
+ dispatch,
507
+ dispatchMessage: rawDispatch,
508
+ } = await setupTestLiveslots(
298
509
  t,
299
510
  buildPromiseWatcherRootObject,
300
511
  'durable-promise-watcher-final',
@@ -303,12 +514,17 @@ test('past-incarnation watched promises', async t => {
303
514
  vatLogs = v.log;
304
515
  vatLogs.length = 0;
305
516
  await dispatchMessage('createLocalPromise', 'final', S);
306
- await dispatchMessage('watchLocalPromise', 'final');
517
+ await dispatchMessage('watchPromise', 'final');
518
+ await dispatchMessage('getWatchResolution', 'final');
307
519
  t.deepEqual(getDispatchLogs(), [
308
520
  fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: final'),
309
521
  subscribeMessage(`p+${nextPExport()}`),
310
- fulfillmentMessage(`p-${nextPImport()}`, 'watched local promise: final'),
522
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: final'),
311
523
  fulfillmentMessage(`p+${lastPExport}`, S),
524
+ fulfillmentMessage(`p-${nextPImport()}`, {
525
+ status: 'fulfilled',
526
+ value: S,
527
+ }),
312
528
  ]);
313
529
  });
314
530
 
@@ -131,6 +131,7 @@ export async function makeDispatch(
131
131
  build,
132
132
  vatID = 'vatA',
133
133
  liveSlotsOptions = {},
134
+ vatParameters = undefined,
134
135
  ) {
135
136
  const gcTools = harden({
136
137
  WeakRef,
@@ -150,7 +151,7 @@ export async function makeDispatch(
150
151
  return { buildRootObject: build };
151
152
  },
152
153
  );
153
- await dispatch(['startVat', kser()]);
154
+ await dispatch(['startVat', kser(vatParameters)]);
154
155
  return { dispatch, testHooks };
155
156
  }
156
157
 
@@ -171,6 +172,7 @@ function makeRPMaker(nextNumber = 1) {
171
172
  * @param {Map<string, string>} [options.kvStore]
172
173
  * @param {number} [options.nextPromiseImportNumber]
173
174
  * @param {boolean} [options.skipLogging]
175
+ * @param {any} [options.vatParameters]
174
176
  */
175
177
  export async function setupTestLiveslots(
176
178
  t,
@@ -183,6 +185,7 @@ export async function setupTestLiveslots(
183
185
  kvStore = new Map(),
184
186
  nextPromiseImportNumber,
185
187
  skipLogging = false,
188
+ vatParameters,
186
189
  } = options;
187
190
  const { log, syscall, fakestore } = buildSyscall({ skipLogging, kvStore });
188
191
  const nextRP = makeRPMaker(nextPromiseImportNumber);
@@ -190,6 +193,8 @@ export async function setupTestLiveslots(
190
193
  syscall,
191
194
  buildRootObject,
192
195
  vatName,
196
+ {},
197
+ vatParameters,
193
198
  );
194
199
 
195
200
  async function dispatchMessage(message, ...args) {
@@ -11,6 +11,7 @@ import {
11
11
  makeStartVat,
12
12
  makeBringOutYourDead,
13
13
  makeResolve,
14
+ makeRetireImports,
14
15
  } from './util.js';
15
16
  import { makeMockGC } from './mock-gc.js';
16
17
 
@@ -465,3 +466,101 @@ for (const firstType of ['object', 'collection']) {
465
466
  }
466
467
 
467
468
  // test('double-free', doublefreetest, { firstType: 'object', lastType: 'collection', order: 'first->last' });
469
+
470
+ test('retirement', async t => {
471
+ const { syscall, fakestore, log } = buildSyscall();
472
+ const gcTools = makeMockGC();
473
+
474
+ // A is a weak collection, with one entry, whose key is B (a
475
+ // Presence). We drop the RAM pillar for B and do a BOYD, which
476
+ // should provoke a syscall.dropImports. Then, when we delete A (by
477
+ // dropping the RAM pillar), the next BOYD should see a
478
+ // `syscall.retireImports`.
479
+
480
+ let weakmapA;
481
+ let presenceB;
482
+
483
+ function buildRootObject(vatPowers) {
484
+ const { VatData } = vatPowers;
485
+ const { makeScalarBigWeakMapStore } = VatData;
486
+
487
+ weakmapA = makeScalarBigWeakMapStore();
488
+
489
+ return Far('root', {
490
+ add: p => {
491
+ presenceB = p;
492
+ weakmapA.init(presenceB, 'value');
493
+ },
494
+ });
495
+ }
496
+
497
+ const makeNS = () => ({ buildRootObject });
498
+ const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, makeNS);
499
+ const { dispatch, testHooks } = ls;
500
+ const { valToSlot } = testHooks;
501
+
502
+ await dispatch(makeStartVat(kser()));
503
+ log.length = 0;
504
+ const weakmapAvref = valToSlot.get(weakmapA);
505
+ const { subid } = parseVatSlot(weakmapAvref);
506
+ const collectionID = String(subid);
507
+
508
+ const rootA = 'o+0';
509
+ const presenceBvref = 'o-1';
510
+ await dispatch(makeMessage(rootA, 'add', [kslot(presenceBvref)]));
511
+ log.length = 0;
512
+
513
+ // the fact that weakmapA can recognize presenceA is recorded in a
514
+ // vatstore key
515
+ const recognizerKey = `vom.ir.${presenceBvref}|${collectionID}`;
516
+ t.is(fakestore.get(recognizerKey), '1');
517
+
518
+ // tell mockGC that userspace has dropped presenceB
519
+ gcTools.kill(presenceB);
520
+ gcTools.flushAllFRs();
521
+
522
+ await dispatch(makeBringOutYourDead());
523
+ const priorKey = `vom.ir.${presenceBvref}|`;
524
+
525
+ t.deepEqual(log.splice(0), [
526
+ // when a Presence is dropped, scanForDeadObjects can't drop the
527
+ // underlying vref import until it knows that virtual data isn't
528
+ // holding a reference, so we expect a refcount check
529
+ { type: 'vatstoreGet', key: `vom.rc.${presenceBvref}`, result: undefined },
530
+
531
+ // the vref is now in importsToDrop, but since this commonly means
532
+ // it can be retired too, scanForDeadObjects goes ahead and checks
533
+ // for recognizers
534
+ { type: 'vatstoreGetNextKey', priorKey, result: recognizerKey },
535
+
536
+ // it found a recognizer, so the vref cannot be retired
537
+ // yet. scanForDeadObjects finishes the BOYD by emitting the
538
+ // dropImports, but should keep watching for an opportunity to
539
+ // retire it too
540
+ { type: 'dropImports', slots: [presenceBvref] },
541
+ ]);
542
+
543
+ // now tell mockGC that we're dropping the weakmap too
544
+ gcTools.kill(weakmapA);
545
+ gcTools.flushAllFRs();
546
+
547
+ // this will provoke the deletion of the collection and all its
548
+ // data. It should *also* trigger a syscall.retireImports of the
549
+ // no-longer-recognizable key
550
+ await dispatch(makeBringOutYourDead());
551
+ const retires = log.filter(e => e.type === 'retireImports');
552
+
553
+ t.deepEqual(retires, [{ type: 'retireImports', slots: [presenceBvref] }]);
554
+
555
+ // If the bug is present, the vat won't send `syscall.retireImports`
556
+ // to the kernel. In a full system, that means the kernel can
557
+ // eventually send a `dispatch.retireImports` into the vat, if/when
558
+ // the object's hosting vat decides to drop it. Make sure that won't
559
+ // cause a crash.
560
+
561
+ if (!retires.length) {
562
+ console.log(`testing kernel's dispatch.retireImports`);
563
+ await dispatch(makeRetireImports(presenceBvref));
564
+ console.log(`dispatch.retireImports did not crash`);
565
+ }
566
+ });
@@ -308,6 +308,14 @@ test.serial('GC dispatch.dropExports', async t => {
308
308
  // that should allow ex1 to be collected
309
309
  t.true(collected.result);
310
310
 
311
+ // upon collection, the vat should scan for local recognizers (weak
312
+ // collection keys) in case any need to be dropped, and find none
313
+ t.deepEqual(log.shift(), {
314
+ type: 'vatstoreGetNextKey',
315
+ priorKey: `vom.ir.${ex1}|`,
316
+ result: 'vom.rc.o+d6/1',
317
+ });
318
+
311
319
  // and once it's collected, the vat should emit `syscall.retireExport`
312
320
  // because nobody else will be able to recognize it again
313
321
  const l2 = log.shift();
@@ -394,8 +402,16 @@ test.serial(
394
402
  // which should let the export be collected
395
403
  t.true(collected.result);
396
404
 
397
- // the vat should *not* emit `syscall.retireExport`, because it already
398
- // received a dispatch.retireExport
405
+ // the vat should scan for local recognizers (weak collection
406
+ // keys) in case any need to be dropped, and find none
407
+ t.deepEqual(log.shift(), {
408
+ type: 'vatstoreGetNextKey',
409
+ priorKey: 'vom.ir.o+10|',
410
+ result: 'vom.rc.o+d6/1',
411
+ });
412
+
413
+ // the vat should *not* emit `syscall.retireExport`, because it
414
+ // already received a dispatch.retireExport
399
415
  t.deepEqual(log, []);
400
416
  },
401
417
  );
@@ -1,10 +1,10 @@
1
1
  // @ts-nocheck
2
2
  import test from 'ava';
3
3
 
4
+ import { Fail } from '@endo/errors';
4
5
  import { E } from '@endo/eventual-send';
5
6
  import { Far } from '@endo/marshal';
6
7
  import { makePromiseKit } from '@endo/promise-kit';
7
- import { Fail } from '@agoric/assert';
8
8
  import { kslot, kser, kunser } from '@agoric/kmarshal';
9
9
  import { M } from '@agoric/store';
10
10
  import { makeLiveSlots, makeMarshaller } from '../src/liveslots.js';