From d64dc3752ec31a365c7bed142b0a8bc77af3b4c8 Mon Sep 17 00:00:00 2001 From: Todd Schiller Date: Thu, 4 Jun 2026 23:08:39 -0400 Subject: [PATCH] Cover storage setRuleEnabled / setAllRuleStates / normalize paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings src/lib/storage.ts from 86.48% to 100% statements (66.66% to 100% functions, 82.35% to 94.11% branches). The existing tests covered DEFAULT_STATES + parseOverrides env-var paths; this PR extends with the missing mutators and the stored-value normalize paths: - parseOverrides null / array / primitive guard (covers the `!parsed || typeof parsed !== "object" || Array.isArray(parsed)` short-circuit that none of the existing cases exercised) - normalize via getRuleStates: corrupt stored values (non-boolean for a rule id) fall back to defaults; missing rule ids fill from defaults - setRuleEnabled: flips one rule, preserves the rest, notifies subscribers - setAllRuleStates: normalizes partial input + drops non-boolean values before writing - subscribe cleanup stops further notifications Ratchet global thresholds 69/64/67/68 → 69/65/70/69. The statements and lines ratchets stay at 69 because src/lib went from 96.11/95.09 to 97.41/97.35 — meaningful but not enough to bump the cross-stratum global past 70%. Co-Authored-By: Claude Opus 4.7 (1M context) --- extension/jest.config.cjs | 6 +- extension/src/lib/__tests__/storage.test.ts | 168 +++++++++++++++++++- 2 files changed, 166 insertions(+), 8 deletions(-) diff --git a/extension/jest.config.cjs b/extension/jest.config.cjs index a5fefa2..be99572 100644 --- a/extension/jest.config.cjs +++ b/extension/jest.config.cjs @@ -100,9 +100,9 @@ module.exports = { coverageThreshold: { global: { statements: 69, - branches: 64, - functions: 67, - lines: 68, + branches: 65, + functions: 70, + lines: 69, }, "./src/rules/": { statements: 91, diff --git a/extension/src/lib/__tests__/storage.test.ts b/extension/src/lib/__tests__/storage.test.ts index ed8529e..23dcf26 100644 --- a/extension/src/lib/__tests__/storage.test.ts +++ b/extension/src/lib/__tests__/storage.test.ts @@ -11,12 +11,15 @@ // the freshly-derived defaults instead of any stale stored value from a // previous case. -import { RULE_DEFAULTS } from "../../rules/rule-defaults.generated"; -import type { getRuleStates as GetRuleStates } from "../storage"; +import { RULE_DEFAULTS as RAW_RULE_DEFAULTS } from "../../rules/rule-defaults.generated"; +import type * as Storage from "../storage"; -interface StorageModule { - getRuleStates: typeof GetRuleStates; -} +type StorageModule = typeof Storage; + +// `RULE_DEFAULTS` is exported as a deeply-typed `as const` object so callers +// see per-key literal types. For these tests we want plain string indexing — +// pick a rule id off Object.keys, look up its default. Widen via this alias. +const RULE_DEFAULTS = RAW_RULE_DEFAULTS as Readonly>; async function loadStorage(): Promise { let module!: StorageModule; @@ -114,4 +117,159 @@ describe("storage default states", () => { const states = await getRuleStates(); expect(states).toEqual(RULE_DEFAULTS); }); + + // parseOverrides rejects anything that isn't a plain object: null, + // arrays, and primitives all decode but should be ignored. Without this + // case the `Array.isArray(parsed)` / typeof guard at the top of + // parseOverrides goes uncovered. + it.each([ + ["null", "null"], + ["an array", JSON.stringify([true, false])], + ["a primitive number", "42"], + ])("ignores EXTENSION_DEFAULT_OVERRIDES that decodes to %s", async (_label, raw) => { + process.env.EXTENSION_DEFAULT_OVERRIDES = raw; + const { getRuleStates } = await loadStorage(); + await expect(getRuleStates()).resolves.toEqual(RULE_DEFAULTS); + }); +}); + +describe("normalize via getRuleStates", () => { + // Pick stable rule ids off the live catalog so the test doesn't tie itself + // to the iteration order of RULE_DEFAULTS. + function pickRuleIds(): { first: string; second: string } { + const ids = Object.keys(RULE_DEFAULTS); + const first = ids[0]; + const second = ids[1]; + if (!first || !second) { + throw new Error("RULE_DEFAULTS must have at least two entries"); + } + return { first, second }; + } + + it("substitutes the codegen default for any non-boolean stored value", async () => { + const { getRuleStates, ruleStatesStorage } = await loadStorage(); + const { first, second } = pickRuleIds(); + // Cast to bypass the typed shape — the whole point is that the storage + // layer's normalize() drops the corrupt string value. + await ruleStatesStorage.set({ + [first]: !RULE_DEFAULTS[first], + [second]: "yes", + } as unknown as Storage.RuleStates); + + const states = await getRuleStates(); + expect(states[first]).toBe(!RULE_DEFAULTS[first]); + expect(states[second]).toBe(RULE_DEFAULTS[second]); + }); + + it("fills in missing rule ids from defaults when stored state is partial", async () => { + const { getRuleStates, ruleStatesStorage } = await loadStorage(); + const { first } = pickRuleIds(); + await ruleStatesStorage.set({ [first]: !RULE_DEFAULTS[first] }); + + const states = await getRuleStates(); + expect(states[first]).toBe(!RULE_DEFAULTS[first]); + // Every other rule still resolves to its codegen default. + for (const [id, value] of Object.entries(RULE_DEFAULTS)) { + if (id === first) { + continue; + } + expect(states[id]).toBe(value); + } + }); +}); + +describe("setRuleEnabled", () => { + it("flips one rule while preserving the others", async () => { + const { getRuleStates, setRuleEnabled } = await loadStorage(); + const target = Object.keys(RULE_DEFAULTS)[0]; + if (!target) { + throw new Error("RULE_DEFAULTS must have at least one entry"); + } + const next = !RULE_DEFAULTS[target]; + + await setRuleEnabled(target, next); + + const states = await getRuleStates(); + expect(states[target]).toBe(next); + for (const [id, value] of Object.entries(RULE_DEFAULTS)) { + if (id === target) { + continue; + } + expect(states[id]).toBe(value); + } + }); + + it("notifies subscribers when state changes", async () => { + const { setRuleEnabled, subscribe } = await loadStorage(); + const target = Object.keys(RULE_DEFAULTS)[0]; + if (!target) { + throw new Error("RULE_DEFAULTS must have at least one entry"); + } + const listener = jest.fn(); + subscribe(listener); + + await setRuleEnabled(target, !RULE_DEFAULTS[target]); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ [target]: !RULE_DEFAULTS[target] }), + ); + }); +}); + +describe("setAllRuleStates", () => { + it("normalizes partial input before writing — missing ids fill from defaults", async () => { + const { getRuleStates, setAllRuleStates } = await loadStorage(); + const target = Object.keys(RULE_DEFAULTS)[0]; + if (!target) { + throw new Error("RULE_DEFAULTS must have at least one entry"); + } + + await setAllRuleStates({ [target]: !RULE_DEFAULTS[target] }); + + const states = await getRuleStates(); + expect(states[target]).toBe(!RULE_DEFAULTS[target]); + for (const [id, value] of Object.entries(RULE_DEFAULTS)) { + if (id === target) { + continue; + } + expect(states[id]).toBe(value); + } + }); + + it("drops non-boolean values via normalize before writing", async () => { + const { getRuleStates, setAllRuleStates } = await loadStorage(); + const target = Object.keys(RULE_DEFAULTS)[0]; + if (!target) { + throw new Error("RULE_DEFAULTS must have at least one entry"); + } + + // Cast to bypass the typed shape — normalize() inside setAllRuleStates + // is responsible for dropping the corrupt string value. + await setAllRuleStates({ + [target]: "junk", + } as unknown as Partial); + + // junk replaced with default → stored map equals defaults. + await expect(getRuleStates()).resolves.toEqual(RULE_DEFAULTS); + }); +}); + +describe("subscribe", () => { + it("stops invoking the listener after the returned cleanup runs", async () => { + const { setRuleEnabled, subscribe } = await loadStorage(); + const target = Object.keys(RULE_DEFAULTS)[0]; + if (!target) { + throw new Error("RULE_DEFAULTS must have at least one entry"); + } + const listener = jest.fn(); + const cleanup = subscribe(listener); + + await setRuleEnabled(target, !RULE_DEFAULTS[target]); + expect(listener).toHaveBeenCalledTimes(1); + + cleanup(); + await setRuleEnabled(target, Boolean(RULE_DEFAULTS[target])); + expect(listener).toHaveBeenCalledTimes(1); + }); });