From 5a586461beff79afe4ec7bbef1d231e6749261ff Mon Sep 17 00:00:00 2001 From: Torlando <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:18:27 +0000 Subject: [PATCH] feat: publish browser map-set activation with read-back ordering --- tests/web/test_map_installer.mjs | 152 ++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/tests/web/test_map_installer.mjs b/tests/web/test_map_installer.mjs index 6a403dd6..bbc27c22 100644 --- a/tests/web/test_map_installer.mjs +++ b/tests/web/test_map_installer.mjs @@ -116,13 +116,16 @@ function storedZip(entries) { } class MemoryFileHandle { - constructor(name, log) { this.name = name; this.kind = 'file'; this.bytes = new Uint8Array(); this.log = log; } + constructor(name, log) { this.name = name; this.kind = 'file'; this.bytes = new Uint8Array(); this.log = log; this.failOnWrite = false; } async createWritable() { const handle = this; let pending = new Uint8Array(); return { async write(value) { pending = new Uint8Array(value instanceof Blob ? await value.arrayBuffer() : value); }, - async close() { handle.bytes = pending; handle.log.push(`write:${handle.name}`); }, + async close() { + if (handle.failOnWrite) throw new Error(`injected write failure: ${handle.name}`); + handle.bytes = pending; handle.log.push(`write:${handle.name}`); + }, async abort() {}, }; } @@ -1291,3 +1294,148 @@ test('map-set capacity is checked before a ninth pack is published', async () => await assert.rejects(installMuiZip({archive,rootDirectory:root,metadata:{...metadata,packId:'pack-nine',name:'Pack Nine'}}), /8-pack/i); assert.equal(pyxis.children.has('packs'), false); }); + +// Seed a card with a valid inherited composition: one published pack and a +// matching active slot, so preflight validation passes for the inherited +// pack before the new install runs. +async function seedInheritedCard(root, slotName, generation, inheritedPackId) { + const style = getMuiStyleProfile('osm-bright'); + const seedMeta = { + ...metadata, packId: inheritedPackId, name: 'Seed Pack', + attribution: style.attribution, source: style.source, license: style.license, + }; + const pyxis = await root.getDirectoryHandle('pyxis-map', {create: true}); + const packs = await pyxis.getDirectoryHandle('packs', {create: true}); + const seed = await packs.getDirectoryHandle(inheritedPackId, {create: true}); + const manifest = serializeIndexlessManifest(seedMeta, 1, 1, 1); + const manifestHandle = await seed.getFileHandle('manifest.pmp', {create: true}); + const manifestWritable = await manifestHandle.createWritable(); + await manifestWritable.write(manifest); + await manifestWritable.close(); + const slotHandle = await pyxis.getFileHandle(slotName, {create: true}); + const slotWritable = await slotHandle.createWritable(); + await slotWritable.write(encodeActiveMapSet({ + generation, mapSetId: 'osm-bright', attribution: style.attribution, + packs: [{packId: inheritedPackId}], + })); + await slotWritable.close(); + return pyxis; +} + +test('activation publishes the style record before the active slot', async () => { + const root = new MemoryDirectoryHandle(); + const pyxis = await seedInheritedCard(root, 'active-pack.0', 5, 'seed-pack'); + await installMuiZip({ + archive: storedZip([['maps/osm-bright/2/1/1.png', PNG]]), + rootDirectory: root, + metadata: {...metadata, packId: 'pack-a'}, + }); + const styleWrite = root.log.findIndex(name => name === 'write:osm-bright.pmas'); + const slotWrite = root.log.findIndex(name => name === 'write:active-pack.1'); + assert.notEqual(styleWrite, -1, 'style record must be written'); + assert.notEqual(slotWrite, -1, 'active slot must be written'); + assert.ok(styleWrite < slotWrite, 'style PMAS must be written before the active slot'); +}); + +test('a failed style write leaves every active slot untouched and is retriable', async () => { + const style = getMuiStyleProfile('osm-bright'); + const root = new MemoryDirectoryHandle(); + const pyxis = await seedInheritedCard(root, 'active-pack.0', 5, 'seed-pack'); + const slot0Before = await fileAt(root, 'pyxis-map/active-pack.0'); + // A corrupt 3-byte style record: present (so the resume check sees a + // file), but undecodable (so the planner ignores it). + const mapSets = await pyxis.getDirectoryHandle('map-sets', {create: true}); + const styleHandle = await mapSets.getFileHandle('osm-bright.pmas', {create: true}); + const corruptWritable = await styleHandle.createWritable(); + await corruptWritable.write(new Uint8Array([0xff, 0x00, 0x13])); + await corruptWritable.close(); + const corruptStyle = await fileAt(root, 'pyxis-map/map-sets/osm-bright.pmas'); + styleHandle.failOnWrite = true; + + await assert.rejects( + installMuiZip({ + archive: storedZip([['maps/osm-bright/2/1/1.png', PNG]]), + rootDirectory: root, + metadata: {...metadata, packId: 'pack-a'}, + }), + /activation failed; retry with the same ZIP and pack ID/i, + ); + styleHandle.failOnWrite = false; + + // The published pack stays on the card (exact retry is possible), the + // existing slot is byte-identical, and no slot was created or clobbered. + assert.equal((await fileAt(root, 'pyxis-map/packs/pack-a/manifest.pmp')).length > 0, true); + assert.equal( + Buffer.from(await fileAt(root, 'pyxis-map/active-pack.0')).toString('hex'), + Buffer.from(slot0Before).toString('hex'), + ); + assert.equal(pyxis.children.has('active-pack.1'), false); + // The failed style write left the original corrupt bytes in place. + assert.equal( + Buffer.from(await fileAt(root, 'pyxis-map/map-sets/osm-bright.pmas')).toString('hex'), + Buffer.from(corruptStyle).toString('hex'), + ); + + // Exact retry converges: the corrupt style file is ignored by the + // planner, the plan is re-derived from the untouched slot, and style + + // slot end up at the same generation. + const resumed = await installMuiZip({ + archive: storedZip([['maps/osm-bright/2/1/1.png', PNG]]), + rootDirectory: root, + metadata: {...metadata, packId: 'pack-a'}, + }); + assert.equal(resumed.resumed, true); + const styleRecord = decodeActiveSelection(await fileAt(root, 'pyxis-map/map-sets/osm-bright.pmas')); + const slot1 = decodeActiveSelection(await fileAt(root, 'pyxis-map/active-pack.1')); + assert.equal(styleRecord.generation, slot1.generation); + assert.deepEqual(slot1.packs.map(pack => pack.packId), ['pack-a', 'seed-pack']); + assert.equal( + Buffer.from(await fileAt(root, 'pyxis-map/active-pack.0')).toString('hex'), + Buffer.from(slot0Before).toString('hex'), + ); +}); + +test('a failed slot write keeps the prior valid slot active and converges on retry', async () => { + const root = new MemoryDirectoryHandle(); + const pyxis = await seedInheritedCard(root, 'active-pack.0', 5, 'seed-pack'); + const slot0Before = await fileAt(root, 'pyxis-map/active-pack.0'); + const slot1Handle = await pyxis.getFileHandle('active-pack.1', {create: true}); + slot1Handle.failOnWrite = true; + + await assert.rejects( + installMuiZip({ + archive: storedZip([['maps/osm-bright/2/1/1.png', PNG]]), + rootDirectory: root, + metadata: {...metadata, packId: 'pack-a'}, + }), + /activation failed; retry with the same ZIP and pack ID/i, + ); + slot1Handle.failOnWrite = false; + + // The style record advanced, but the failed slot write left the prior + // valid slot (active-pack.0) intact as the fallback the firmware can use. + const styleRecord = decodeActiveSelection(await fileAt(root, 'pyxis-map/map-sets/osm-bright.pmas')); + assert.equal(styleRecord.generation, 6); + assert.equal( + Buffer.from(await fileAt(root, 'pyxis-map/active-pack.0')).toString('hex'), + Buffer.from(slot0Before).toString('hex'), + ); + assert.equal((await fileAt(root, 'pyxis-map/active-pack.1')).length, 0); + + // Exact retry re-plans from the advanced style record and converges: + // both records carry the same new generation. + await installMuiZip({ + archive: storedZip([['maps/osm-bright/2/1/1.png', PNG]]), + rootDirectory: root, + metadata: {...metadata, packId: 'pack-a'}, + }); + const retryStyle = decodeActiveSelection(await fileAt(root, 'pyxis-map/map-sets/osm-bright.pmas')); + const retrySlot = decodeActiveSelection(await fileAt(root, 'pyxis-map/active-pack.1')); + assert.equal(retryStyle.generation, retrySlot.generation); + assert.equal(retryStyle.generation, 7); + assert.deepEqual(retrySlot.packs.map(pack => pack.packId), ['pack-a', 'seed-pack']); + assert.equal( + Buffer.from(await fileAt(root, 'pyxis-map/active-pack.0')).toString('hex'), + Buffer.from(slot0Before).toString('hex'), + ); +});