diff --git a/apps/desktop/extensions/ai-sidebar/moshpit.js b/apps/desktop/extensions/ai-sidebar/moshpit.js index 4dc3401..4a2078a 100644 --- a/apps/desktop/extensions/ai-sidebar/moshpit.js +++ b/apps/desktop/extensions/ai-sidebar/moshpit.js @@ -77,17 +77,45 @@ export function moshpitBypassHosts(config) { return hosts; } +/** + * Bases that a previous version shipped as its default and that must not + * survive an upgrade. + * + * A stored value beats the shipped default — that is the point of storing one — + * but it makes a settings write indistinguishable from a deliberate choice. An + * install that ever persisted moshcoding.com as its parking base keeps pointing + * at a route that has never existed, through every future release, and no + * amount of shipping the right default fixes it. + * + * So a stored value that merely repeats a superseded default is treated as + * absent. A base someone actually chose is untouched, because it will not be on + * this list. + */ +const SUPERSEDED_BASES = new Set([ + 'https://moshcoding.com', // parking, before /n/ existed + 'http://moshcoding.com', +]); + +/** A stored base, unless it is a stale default in disguise. */ +function storedBase(value, fallback) { + const base = String(value || '').trim().replace(/\/+$/, ''); + if (!base || SUPERSEDED_BASES.has(base)) return fallback.replace(/\/+$/, ''); + return base; +} + /** Read the settings the options page writes. */ export async function moshpitConfig() { const { moshpitConfig: cfg } = await chrome.storage.local.get('moshpitConfig'); return { mode: cfg?.mode === 'moshpit' ? 'moshpit' : 'clearnet', - registryBase: (cfg?.registryBase || DEFAULT_REGISTRY_BASE).replace(/\/+$/, ''), - consoleBase: (cfg?.consoleBase || DEFAULT_CONSOLE_BASE).replace(/\/+$/, ''), - parkingBase: (cfg?.parkingBase || DEFAULT_PARKING_BASE).replace(/\/+$/, ''), + registryBase: storedBase(cfg?.registryBase, DEFAULT_REGISTRY_BASE), + consoleBase: storedBase(cfg?.consoleBase, DEFAULT_CONSOLE_BASE), + parkingBase: storedBase(cfg?.parkingBase, DEFAULT_PARKING_BASE), }; } +export { SUPERSEDED_BASES, storedBase }; + /** * Split a hostname the way the registry does: exactly one label and one TLD. * Anything else (`a.b.c`, a bare `localhost`, an IP) is not a Moshpit name and diff --git a/apps/desktop/extensions/ai-sidebar/moshpit.test.js b/apps/desktop/extensions/ai-sidebar/moshpit.test.js index 3481b7c..8b74628 100644 --- a/apps/desktop/extensions/ai-sidebar/moshpit.test.js +++ b/apps/desktop/extensions/ai-sidebar/moshpit.test.js @@ -260,3 +260,34 @@ describe('parking sends a name somewhere that exists', () => { expect(js.parkingUrlFor('a b.eggs')).toBe('https://pit.moshcode.sh/n/a%20b.eggs'); }); }); + +describe('a stored setting must not outlive the default it copied', () => { + it('ignores a base that is only a superseded default', () => { + // The bug this fixes: an install that ever persisted moshcoding.com keeps + // pointing at a route that has never existed, through every future + // release, and shipping the right default does nothing about it. + expect(js.storedBase('https://moshcoding.com', 'https://pit.moshcode.sh')) + .toBe('https://pit.moshcode.sh'); + expect(js.storedBase('http://moshcoding.com', 'https://pit.moshcode.sh')) + .toBe('https://pit.moshcode.sh'); + expect(js.storedBase('https://moshcoding.com/', 'https://pit.moshcode.sh')) + .toBe('https://pit.moshcode.sh', 'a trailing slash is the same value'); + }); + + it('keeps a base someone actually chose', () => { + // Only stale defaults are on the list, so a real choice is never on it. + expect(js.storedBase('https://my.pit', 'https://pit.moshcode.sh')).toBe('https://my.pit'); + expect(js.storedBase('https://my.pit/', 'https://pit.moshcode.sh')).toBe('https://my.pit'); + }); + + it('falls through to the shipped default when nothing is stored', () => { + for (const empty of ['', ' ', null, undefined]) { + expect(js.storedBase(empty, 'https://pit.moshcode.sh')).toBe('https://pit.moshcode.sh'); + } + }); + + it('lists the base that caused this', () => { + expect(js.SUPERSEDED_BASES.has('https://moshcoding.com')).toBe(true); + expect(js.SUPERSEDED_BASES.has('https://pit.moshcode.sh')).toBe(false); + }); +}); diff --git a/apps/desktop/extensions/ai-sidebar/options.js b/apps/desktop/extensions/ai-sidebar/options.js index 8ffe2dc..33b05e2 100644 --- a/apps/desktop/extensions/ai-sidebar/options.js +++ b/apps/desktop/extensions/ai-sidebar/options.js @@ -388,12 +388,20 @@ el("syncUrl").addEventListener("change", async () => { }); }); async function saveMoshpit() { - await chrome.storage.local.set({ - moshpitConfig: { - mode: el("moshpitMode").value === "moshpit" ? "moshpit" : "clearnet", - registryBase: el("moshpitRegistry").value.trim(), - }, - }); + // Only what was actually filled in. Writing an empty field would store "", + // and a stored value — even an empty one — is a decision this code then has + // to keep honouring. Leaving a field out is what lets it keep following + // whatever the shipped default becomes. + const registryBase = el("moshpitRegistry").value.trim(); + const previous = (await chrome.storage.local.get("moshpitConfig")).moshpitConfig || {}; + const next = { + ...previous, + mode: el("moshpitMode").value === "moshpit" ? "moshpit" : "clearnet", + }; + if (registryBase) next.registryBase = registryBase; + else delete next.registryBase; + + await chrome.storage.local.set({ moshpitConfig: next }); flash( "savedMoshpit", el("moshpitMode").value === "moshpit"