Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/gittensory-miner-extension/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions packages/gittensory-miner/docs/env-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | `""` |
Expand Down
33 changes: 29 additions & 4 deletions test/unit/miner-extension-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = { watchedRepos: [] };
it("purges a discoveryIndexUrl value already synced by an older extension version, on load and on save", async () => {
const synced: Record<string, unknown> = {
watchedRepos: [],
discoveryIndexUrl: "https://legacy.example.test/index.json",
};
const setCalls: Array<Record<string, unknown>> = [];
const removeCalls: string[] = [];
const elements = {
"#settings": createFormMock(),
"#status": { textContent: "" },
Expand All @@ -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 () => {} },
},
Expand All @@ -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 {
Expand Down Expand Up @@ -308,5 +332,6 @@ function loadOptionsInternals() {
return vmContext.__gittensoryMinerOptionsInternals as {
parseWatchedRepos: (text: string) => string[];
parseRankedCandidatesJson: (text: string) => unknown[];
removeLegacyDiscoveryIndexUrl: () => Promise<void>;
};
}