diff --git a/apps/gittensory-miner-extension/options.js b/apps/gittensory-miner-extension/options.js index 6e2c5062ed..694b006953 100644 --- a/apps/gittensory-miner-extension/options.js +++ b/apps/gittensory-miner-extension/options.js @@ -15,10 +15,20 @@ function parseRankedCandidatesJson(text) { return parsed; } +// #5343 dropped the discoveryIndexUrl UI field and stopped reading/writing it, but chrome.storage.sync.set +// only merges keys -- it never deletes ones an earlier extension version already synced. Without an active +// purge, a value synced before #5343 stays in the user's account indefinitely. Called from refreshSettings, +// which runs on every options-page load and again at the end of every save, so it's cleared promptly +// regardless of which path a given user hits first. +async function removeLegacyDiscoveryIndexUrl() { + await chrome.storage.sync.remove("discoveryIndexUrl"); +} + if (globalThis.__GITTENSORY_MINER_EXTENSION_TEST__) { globalThis.__gittensoryMinerOptionsInternals = { parseWatchedRepos, parseRankedCandidatesJson, + removeLegacyDiscoveryIndexUrl, }; } @@ -53,6 +63,7 @@ form.addEventListener("submit", async (event) => { async function refreshSettings() { const stored = await chrome.storage.sync.get({ watchedRepos: [] }); + await removeLegacyDiscoveryIndexUrl(); const local = await chrome.storage.local.get({ rankedCandidates: [] }); const repos = Array.isArray(stored.watchedRepos) ? stored.watchedRepos : []; watchedRepos.value = repos.join("\n"); diff --git a/packages/gittensory-miner/docs/env-reference.md b/packages/gittensory-miner/docs/env-reference.md index e5e9d47277..f34d768564 100644 --- a/packages/gittensory-miner/docs/env-reference.md +++ b/packages/gittensory-miner/docs/env-reference.md @@ -12,6 +12,7 @@ Generated by `npm run miner:env-reference`. Do not edit manually. | `GITTENSORY_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.js` | (none) | | `GITTENSORY_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.js` | `""` | | `GITTENSORY_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.js` | (none) | +| `GITTENSORY_MINER_KILL_SWITCH` | `lib/config-precedence.js` | `""` | | `GITTENSORY_MINER_NO_UPDATE_CHECK` | `lib/update-check.js` | `""` | | `GITTENSORY_MINER_ORB_EXPORT_DB` | `lib/orb-export.js` | `""` | | `GITTENSORY_MINER_PLAN_STORE_DB` | `lib/plan-store.js` | `""` | diff --git a/test/unit/miner-extension-content.test.ts b/test/unit/miner-extension-content.test.ts index a192aada23..cef291fc30 100644 --- a/test/unit/miner-extension-content.test.ts +++ b/test/unit/miner-extension-content.test.ts @@ -148,15 +148,18 @@ describe("miner extension opportunity badge", () => { expect(() => internals.parseRankedCandidatesJson('{"not":"array"}')).toThrow(); }); - it("REGRESSION (dead-field removal): no discoveryIndexUrl config field remains anywhere in the extension", () => { + it("REGRESSION (dead-field removal): no discoveryIndexUrl config field remains in the UI or background reads", () => { expect(optionsHtml).not.toMatch(/discoveryIndexUrl/); - expect(optionsScript).not.toMatch(/discoveryIndexUrl/); expect(backgroundScript).not.toMatch(/discoveryIndexUrl/); }); - it("saves and restores settings without ever writing or reading discoveryIndexUrl", async () => { - const synced: Record = { watchedRepos: [] }; + it("purges a discoveryIndexUrl value already synced by an older extension version, on load and on save", async () => { + const synced: Record = { + watchedRepos: [], + discoveryIndexUrl: "https://legacy.example.test/index.json", + }; const setCalls: Array> = []; + const removeCalls: string[] = []; const elements = { "#settings": createFormMock(), "#status": { textContent: "" }, @@ -174,6 +177,10 @@ describe("miner extension opportunity badge", () => { setCalls.push(value); Object.assign(synced, value); }, + remove: async (key: string) => { + removeCalls.push(key); + delete synced[key]; + }, }, local: { get: async () => ({ rankedCandidates: [] }), set: async () => {} }, }, @@ -184,15 +191,32 @@ describe("miner extension opportunity badge", () => { const vmContext = createContext(context); new Script(optionsScript).runInContext(vmContext); + // The load-time refreshSettings() the script triggers on evaluation already removed it. + await flushPromises(); + expect(removeCalls).toEqual(["discoveryIndexUrl"]); + expect("discoveryIndexUrl" in synced).toBe(false); + + // Re-seed as if another synced device still has the legacy key, then confirm save also purges it. + synced.discoveryIndexUrl = "https://legacy.example.test/index.json"; elements["#watchedRepos"].value = "JSONbored/gittensory"; await elements["#settings"].dispatchSubmit(); expect(setCalls).toHaveLength(1); expect(setCalls[0]).toEqual({ watchedRepos: ["JSONbored/gittensory"] }); + expect(removeCalls).toEqual(["discoveryIndexUrl", "discoveryIndexUrl"]); expect("discoveryIndexUrl" in synced).toBe(false); }); + + it("directly exposes removeLegacyDiscoveryIndexUrl for the internal purge, not a UI-facing setting", () => { + const internals = loadOptionsInternals(); + expect(typeof internals.removeLegacyDiscoveryIndexUrl).toBe("function"); + }); }); +function flushPromises() { + return new Promise((resolve) => setTimeout(resolve, 0)); +} + function createFormMock() { let submitHandler: ((event: { preventDefault: () => void }) => unknown) | null = null; return { @@ -308,5 +332,6 @@ function loadOptionsInternals() { return vmContext.__gittensoryMinerOptionsInternals as { parseWatchedRepos: (text: string) => string[]; parseRankedCandidatesJson: (text: string) => unknown[]; + removeLegacyDiscoveryIndexUrl: () => Promise; }; }