diff --git a/src/lib/selector-prefs-storage.test.ts b/src/lib/selector-prefs-storage.test.ts new file mode 100644 index 0000000000..514ca59550 --- /dev/null +++ b/src/lib/selector-prefs-storage.test.ts @@ -0,0 +1,76 @@ +import { beforeEach, describe, expect, it } from "vitest" + +import { + getSavedModeId, + getSavedPrefsForConnect, + saveConfigPreference, + saveModePreference, +} from "./selector-prefs-storage" + +const STORAGE_KEY = "codeg:selector-prefs" + +beforeEach(() => { + localStorage.clear() +}) + +describe("selector preference persistence", () => { + it("round-trips mode and config preferences", () => { + saveModePreference("codex", { + current_mode_id: "plan", + available_modes: [], + }) + saveConfigPreference("codex", "reasoning_effort", "high") + + expect(getSavedModeId("codex")).toBe("plan") + expect(getSavedPrefsForConnect("codex")).toEqual({ + modeId: "plan", + configValues: { reasoning_effort: "high" }, + }) + }) + + it.each(["{not json", "null", "[]", '"text"', "42"])( + "falls back safely for an invalid top-level value: %s", + (stored) => { + localStorage.setItem(STORAGE_KEY, stored) + + expect(getSavedModeId("codex")).toBeNull() + expect(getSavedPrefsForConnect("codex")).toEqual({ + modeId: null, + configValues: null, + }) + + expect(() => + saveModePreference("codex", { + current_mode_id: "default", + available_modes: [], + }) + ).not.toThrow() + expect(getSavedModeId("codex")).toBe("default") + } + ) + + it("keeps valid nested fields and drops values with the wrong type", () => { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + codex: { + modeId: 7, + configValues: { + reasoning_effort: "medium", + approval_policy: false, + }, + }, + claude_code: ["plan"], + }) + ) + + expect(getSavedPrefsForConnect("codex")).toEqual({ + modeId: null, + configValues: { reasoning_effort: "medium" }, + }) + expect(getSavedPrefsForConnect("claude_code")).toEqual({ + modeId: null, + configValues: null, + }) + }) +}) diff --git a/src/lib/selector-prefs-storage.ts b/src/lib/selector-prefs-storage.ts index 6c20368a14..c6e5965321 100644 --- a/src/lib/selector-prefs-storage.ts +++ b/src/lib/selector-prefs-storage.ts @@ -28,11 +28,43 @@ interface SelectorPrefs { type AllPrefs = Record +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function parseConfigValues(value: unknown): Record | undefined { + if (!isRecord(value)) return undefined + + const entries = Object.entries(value).filter( + (entry): entry is [string, string] => typeof entry[1] === "string" + ) + return entries.length > 0 ? Object.fromEntries(entries) : undefined +} + +function parseAllPrefs(value: unknown): AllPrefs { + if (!isRecord(value)) return {} + + const entries: Array<[string, SelectorPrefs]> = [] + for (const [agentType, rawPrefs] of Object.entries(value)) { + if (!isRecord(rawPrefs)) continue + + const prefs: SelectorPrefs = {} + if (typeof rawPrefs.modeId === "string") { + prefs.modeId = rawPrefs.modeId + } + const configValues = parseConfigValues(rawPrefs.configValues) + if (configValues) prefs.configValues = configValues + entries.push([agentType, prefs]) + } + + return Object.fromEntries(entries) +} + function readAll(): AllPrefs { if (typeof window === "undefined") return {} try { const raw = localStorage.getItem(STORAGE_KEY) - return raw ? (JSON.parse(raw) as AllPrefs) : {} + return raw ? parseAllPrefs(JSON.parse(raw) as unknown) : {} } catch { return {} } @@ -67,7 +99,7 @@ function updatePrefs( // ── Read ── -/** Read saved mode id for an agent (no validation, just the raw value). */ +/** Read the validated saved mode id for an agent. */ export function getSavedModeId(agentType: string): string | null { const all = readAll() return all[agentType]?.modeId ?? null