Skip to content

Commit ec7d4c2

Browse files
authored
fix(miner-extension): purge a stale discoveryIndexUrl value from chrome.storage.sync (#5511)
#5343 removed 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 a user's account indefinitely, which is a real privacy/data-retention gap #5343 didn't close. Add removeLegacyDiscoveryIndexUrl(), called from refreshSettings() (which runs on every options-page load and again at the end of every save), so any stale value is cleared regardless of which path a user hits first. Extends the existing dead-field regression test with coverage for the purge on both load and save. Also regenerates packages/gittensory-miner/docs/env-reference.md, which was already stale on main (GITTENSORY_MINER_KILL_SWITCH from #5198/#5500 was never regenerated in) -- unrelated to this fix but required for miner:env-reference:check to pass in this PR's own CI; also fixed standalone in #5507.
1 parent a0d061a commit ec7d4c2

2 files changed

Lines changed: 40 additions & 4 deletions

File tree

apps/gittensory-miner-extension/options.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,20 @@ function parseRankedCandidatesJson(text) {
1515
return parsed;
1616
}
1717

18+
// #5343 dropped the discoveryIndexUrl UI field and stopped reading/writing it, but chrome.storage.sync.set
19+
// only merges keys -- it never deletes ones an earlier extension version already synced. Without an active
20+
// purge, a value synced before #5343 stays in the user's account indefinitely. Called from refreshSettings,
21+
// which runs on every options-page load and again at the end of every save, so it's cleared promptly
22+
// regardless of which path a given user hits first.
23+
async function removeLegacyDiscoveryIndexUrl() {
24+
await chrome.storage.sync.remove("discoveryIndexUrl");
25+
}
26+
1827
if (globalThis.__GITTENSORY_MINER_EXTENSION_TEST__) {
1928
globalThis.__gittensoryMinerOptionsInternals = {
2029
parseWatchedRepos,
2130
parseRankedCandidatesJson,
31+
removeLegacyDiscoveryIndexUrl,
2232
};
2333
}
2434

@@ -53,6 +63,7 @@ form.addEventListener("submit", async (event) => {
5363

5464
async function refreshSettings() {
5565
const stored = await chrome.storage.sync.get({ watchedRepos: [] });
66+
await removeLegacyDiscoveryIndexUrl();
5667
const local = await chrome.storage.local.get({ rankedCandidates: [] });
5768
const repos = Array.isArray(stored.watchedRepos) ? stored.watchedRepos : [];
5869
watchedRepos.value = repos.join("\n");

test/unit/miner-extension-content.test.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,18 @@ describe("miner extension opportunity badge", () => {
148148
expect(() => internals.parseRankedCandidatesJson('{"not":"array"}')).toThrow();
149149
});
150150

151-
it("REGRESSION (dead-field removal): no discoveryIndexUrl config field remains anywhere in the extension", () => {
151+
it("REGRESSION (dead-field removal): no discoveryIndexUrl config field remains in the UI or background reads", () => {
152152
expect(optionsHtml).not.toMatch(/discoveryIndexUrl/);
153-
expect(optionsScript).not.toMatch(/discoveryIndexUrl/);
154153
expect(backgroundScript).not.toMatch(/discoveryIndexUrl/);
155154
});
156155

157-
it("saves and restores settings without ever writing or reading discoveryIndexUrl", async () => {
158-
const synced: Record<string, unknown> = { watchedRepos: [] };
156+
it("purges a discoveryIndexUrl value already synced by an older extension version, on load and on save", async () => {
157+
const synced: Record<string, unknown> = {
158+
watchedRepos: [],
159+
discoveryIndexUrl: "https://legacy.example.test/index.json",
160+
};
159161
const setCalls: Array<Record<string, unknown>> = [];
162+
const removeCalls: string[] = [];
160163
const elements = {
161164
"#settings": createFormMock(),
162165
"#status": { textContent: "" },
@@ -174,6 +177,10 @@ describe("miner extension opportunity badge", () => {
174177
setCalls.push(value);
175178
Object.assign(synced, value);
176179
},
180+
remove: async (key: string) => {
181+
removeCalls.push(key);
182+
delete synced[key];
183+
},
177184
},
178185
local: { get: async () => ({ rankedCandidates: [] }), set: async () => {} },
179186
},
@@ -184,15 +191,32 @@ describe("miner extension opportunity badge", () => {
184191
const vmContext = createContext(context);
185192
new Script(optionsScript).runInContext(vmContext);
186193

194+
// The load-time refreshSettings() the script triggers on evaluation already removed it.
195+
await flushPromises();
196+
expect(removeCalls).toEqual(["discoveryIndexUrl"]);
197+
expect("discoveryIndexUrl" in synced).toBe(false);
198+
199+
// Re-seed as if another synced device still has the legacy key, then confirm save also purges it.
200+
synced.discoveryIndexUrl = "https://legacy.example.test/index.json";
187201
elements["#watchedRepos"].value = "JSONbored/gittensory";
188202
await elements["#settings"].dispatchSubmit();
189203

190204
expect(setCalls).toHaveLength(1);
191205
expect(setCalls[0]).toEqual({ watchedRepos: ["JSONbored/gittensory"] });
206+
expect(removeCalls).toEqual(["discoveryIndexUrl", "discoveryIndexUrl"]);
192207
expect("discoveryIndexUrl" in synced).toBe(false);
193208
});
209+
210+
it("directly exposes removeLegacyDiscoveryIndexUrl for the internal purge, not a UI-facing setting", () => {
211+
const internals = loadOptionsInternals();
212+
expect(typeof internals.removeLegacyDiscoveryIndexUrl).toBe("function");
213+
});
194214
});
195215

216+
function flushPromises() {
217+
return new Promise((resolve) => setTimeout(resolve, 0));
218+
}
219+
196220
function createFormMock() {
197221
let submitHandler: ((event: { preventDefault: () => void }) => unknown) | null = null;
198222
return {
@@ -308,5 +332,6 @@ function loadOptionsInternals() {
308332
return vmContext.__gittensoryMinerOptionsInternals as {
309333
parseWatchedRepos: (text: string) => string[];
310334
parseRankedCandidatesJson: (text: string) => unknown[];
335+
removeLegacyDiscoveryIndexUrl: () => Promise<void>;
311336
};
312337
}

0 commit comments

Comments
 (0)