diff --git a/src/layers/ghostMaps/edgeCases.test.mjs b/src/layers/ghostMaps/edgeCases.test.mjs
new file mode 100644
index 000000000..21fa7c3c2
--- /dev/null
+++ b/src/layers/ghostMaps/edgeCases.test.mjs
@@ -0,0 +1,187 @@
+// Edge cases from docs/migration/planet-separation/layers.md (s2-ghostmaps):
+// change the KMZ edition and the engine before parsing finishes; only the
+// selected edition is shown, and a bad archive does not erase the working one.
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { createGhostMapsLayer } from './index.js';
+
+function element(localName, children = [], value = '') {
+ const node = {
+ localName,
+ children,
+ get textContent() {
+ return value + children.map((child) => child.textContent).join('');
+ },
+ getAttribute: () => null,
+ getElementsByTagName(name) {
+ return children.flatMap((child) => [
+ ...(name === '*' || name === child.localName ? [child] : []),
+ ...child.getElementsByTagName(name),
+ ]);
+ },
+ };
+ for (const child of children) child.parentElement = node;
+ return node;
+}
+
+// The XML double turns `NAME` into one placemark called NAME.
+function documentFor(xml) {
+ const name = /(.*)<\/kml>/.exec(xml)?.[1] ?? 'unnamed';
+ return element('document', [
+ element('kml', [
+ element('Document', [
+ element('Placemark', [
+ element('name', [], name),
+ element('Point', [element('coordinates', [], '12,34')]),
+ ]),
+ ]),
+ ]),
+ ]);
+}
+
+/** A stored (uncompressed) KMZ holding doc.kml only. */
+function kmz(name) {
+ const key = Buffer.from('doc.kml');
+ const bytes = Buffer.from(`${name}`);
+ const header = Buffer.alloc(30);
+ header.writeUInt32LE(0x04034b50);
+ header.writeUInt32LE(bytes.length, 18);
+ header.writeUInt32LE(bytes.length, 22);
+ header.writeUInt16LE(key.length, 26);
+ const directory = Buffer.alloc(46);
+ directory.writeUInt32LE(0x02014b50);
+ directory.writeUInt32LE(bytes.length, 20);
+ directory.writeUInt32LE(bytes.length, 24);
+ directory.writeUInt16LE(key.length, 28);
+ directory.writeUInt32LE(0, 42);
+ const central = Buffer.concat([directory, key]);
+ const end = Buffer.alloc(22);
+ end.writeUInt32LE(0x06054b50);
+ end.writeUInt16LE(1, 8);
+ end.writeUInt16LE(1, 10);
+ end.writeUInt32LE(central.length, 12);
+ end.writeUInt32LE(header.length + key.length + bytes.length, 16);
+ return new Uint8Array(Buffer.concat([header, key, bytes, central, end]));
+}
+
+function deferred() {
+ let resolve;
+ const promise = new Promise((done) => (resolve = done));
+ return { promise, resolve };
+}
+
+const catalog = (version) => ({
+ products: [{ id: 'cip', version, defaultOn: true }],
+});
+
+/** A scene-less renderer double that records live paints per engine. */
+function fakeRenderer(name, live) {
+ return {
+ name,
+ requestRender() {},
+ onClick: () => () => {},
+ draw(id, features) {
+ const paint = {
+ engine: name,
+ id,
+ names: features.map((feature) => feature.name),
+ ready: Promise.resolve(),
+ destroy() {
+ live.delete(paint);
+ },
+ };
+ live.add(paint);
+ return paint;
+ },
+ destroy() {},
+ };
+}
+
+function harness(t) {
+ const previous = Object.getOwnPropertyDescriptor(globalThis, 'DOMParser');
+ globalThis.DOMParser = class {
+ parseFromString(xml) {
+ return documentFor(xml);
+ }
+ };
+ t.after(() => {
+ if (previous) Object.defineProperty(globalThis, 'DOMParser', previous);
+ else delete globalThis.DOMParser;
+ });
+ t.mock.method(console, 'warn', () => {});
+ const files = new Map();
+ const live = new Set();
+ let listener = null;
+ const layer = createGhostMapsLayer({
+ storage: null,
+ createRenderer: (engine) => fakeRenderer(engine.name, live),
+ source: {
+ getCatalog: async () => catalog('v1'),
+ getFile: (_id, version) => files.get(version).promise,
+ subscribe(onCatalog) {
+ listener = onCatalog;
+ return () => {};
+ },
+ },
+ });
+ layer.init(null, { engine: { name: 'cesium' } });
+ layer.enable();
+ t.after(() => layer.destroy());
+ const file = (version) => {
+ const pending = deferred();
+ files.set(version, pending);
+ return pending;
+ };
+ const push = (next) => listener(next);
+ return { layer, live, file, push };
+}
+
+const names = (layer) => layer.getAnalystRecords().map((record) => record.name);
+const painted = (live) =>
+ [...live].map((paint) => `${paint.engine}:${paint.names.join(',')}`);
+
+test('edge:s2-ghostmaps: an edition and engine change mid-parse shows only the selected edition', async (t) => {
+ const { layer, live, file, push } = harness(t);
+ file('v1').resolve(kmz('edition one'));
+ await layer.update();
+ assert.deepEqual(names(layer), ['edition one']);
+
+ // v2 is still downloading when v3 is published and the engine is swapped.
+ const v2 = file('v2');
+ const v3 = file('v3');
+ push(catalog('v2'));
+ await new Promise(setImmediate);
+ push(catalog('v3'));
+ layer.detachRenderer();
+ await layer.attachRenderer(null, { engine: { name: 'navara' } });
+ v2.resolve(kmz('edition two'));
+ v3.resolve(kmz('edition three'));
+ // Let the queued syncs settle.
+ for (let i = 0; i < 20; i += 1) await new Promise(setImmediate);
+
+ assert.deepEqual(names(layer), ['edition three']);
+ assert.deepEqual(painted(live), ['navara:edition three']);
+ assert.equal(layer.getStats().loading, false);
+ assert.equal(layer.getStats().error, null);
+});
+
+test('edge:s2-ghostmaps: a bad archive keeps the working edition on the globe', async (t) => {
+ const { layer, live, file, push } = harness(t);
+ file('v1').resolve(kmz('working edition'));
+ await layer.update();
+ assert.deepEqual(painted(live), ['cesium:working edition']);
+
+ const broken = file('v2');
+ push(catalog('v2'));
+ // The engine changes while the broken file is still on its way.
+ layer.detachRenderer();
+ await layer.attachRenderer(null, { engine: { name: 'navara' } });
+ broken.resolve(new Uint8Array([1, 2, 3, 4]));
+ for (let i = 0; i < 20; i += 1) await new Promise(setImmediate);
+
+ assert.deepEqual(names(layer), ['working edition']);
+ assert.deepEqual(painted(live), ['navara:working edition']);
+ assert.ok(layer.getStats().error, 'the failed edition is reported');
+ const chip = layer.getRowControls().chips[0];
+ assert.equal(chip.state, 'error');
+});