diff --git a/packages/internal/src/storage-test-utils.js b/packages/internal/src/storage-test-utils.js index 3bac5fb9a7d..049a78a87ff 100644 --- a/packages/internal/src/storage-test-utils.js +++ b/packages/internal/src/storage-test-utils.js @@ -6,6 +6,18 @@ import { bindAllMethods } from './method-tools.js'; const { Fail, quote: q } = assert; +/** + * A map corresponding with a total function such that `get(key)` + * is assumed to always succeed. + * + * @template K, V + * @typedef {{[k in Exclude, 'get'>]: Map[k]} & {get: (key: K) => V}} TotalMap +*/ +/** + * @template T + * @typedef {T extends Map ? TotalMap : never} TotalMapFrom +*/ + /** * A convertSlotToVal function that produces basic Remotables * @@ -72,7 +84,7 @@ export const slotStringUnserialize = makeSlotStringUnserialize(); */ export const makeFakeStorageKit = (rootPath, rootOptions) => { const resolvedOptions = { sequence: true, ...rootOptions }; - /** @type {Map} */ + /** @type {TotalMap} */ const data = new Map(); /** @type {import('../src/lib-chainStorage.js').StorageMessage[]} */ const messages = []; @@ -82,14 +94,35 @@ export const makeFakeStorageKit = (rootPath, rootOptions) => { messages.push(message); switch (message.method) { case 'getStoreKey': { - return { - storeName: 'swingset', - storeSubkey: `fake:${message.args[0]}`, - }; + const [key] = message.args; + return { storeName: 'swingset', storeSubkey: `fake:${key}` }; + } + case 'get': { + const [key] = message.args; + return data.has(key) ? data.get(key) : null; + } + case 'entries': { + const [key] = message.args; + const prefix = `${key}.`; + const childData = new Map(); + for (const [path, value] of data.entries()) { + if (!path.startsWith(prefix)) { + continue; + } + const [segment, ...suffix] = path.slice(prefix.length).split('.'); + if (suffix.length === 0) { + childData.set(segment, value); + } else if (!childData.has(segment)) { + childData.set(segment, null); + } + } + return [...childData.entries()].map(entry => + entry[1] != null ? entry : [entry[0]], + ); } case 'set': for (const [key, value] of message.args) { - if (value !== undefined) { + if (value != null) { data.set(key, value); } else { data.delete(key); @@ -98,12 +131,12 @@ export const makeFakeStorageKit = (rootPath, rootOptions) => { break; case 'append': for (const [key, value] of message.args) { - if (value === undefined) { + if (value == null) { throw Error(`attempt to append with no value`); } let sequence = data.get(key); if (!Array.isArray(sequence)) { - if (sequence === undefined) { + if (sequence == null) { // Initialize an empty collection. sequence = []; } else { diff --git a/packages/internal/test/test-storage-test-utils.js b/packages/internal/test/test-storage-test-utils.js index 0942e1177e2..30fded31a54 100644 --- a/packages/internal/test/test-storage-test-utils.js +++ b/packages/internal/test/test-storage-test-utils.js @@ -13,7 +13,7 @@ import { test('makeFakeStorageKit', async t => { const rootPath = 'root'; const opts = { sequence: false }; - const { rootNode, messages } = makeFakeStorageKit(rootPath, opts); + const { rootNode, messages, toStorage } = makeFakeStorageKit(rootPath, opts); t.is(rootNode.getPath(), rootPath); const rootStoreKey = await rootNode.getStoreKey(); t.deepEqual( @@ -151,6 +151,16 @@ test('makeFakeStorageKit', async t => { [{ method: 'set', args: [[deepPath, 'foo']] }], 'level-skipping setValue message', ); + t.deepEqual( + await toStorage({ method: 'entries', args: [`${rootPath}.child`] }), + [['grandchild', 'foo']], + 'child entries', + ); + t.deepEqual( + await toStorage({ method: 'entries', args: [rootPath] }), + [...extremeSegments.map(segment => [segment, 'foo']), ['child']], + 'entries include empty non-terminals', + ); await childNode.setValue(''); t.deepEqual(