import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test" type PersistTestingType = typeof import("./persist").PersistTesting type PersistType = typeof import("./persist").Persist type RemovePersistedType = typeof import("./persist").removePersisted class MemoryStorage implements Storage { private values = new Map() readonly events: string[] = [] readonly calls = { get: 0, set: 0, remove: 0 } clear() { this.values.clear() } get length() { return this.values.size } key(index: number) { return Array.from(this.values.keys())[index] ?? null } getItem(key: string) { this.calls.get += 1 this.events.push(`get:${key}`) if (key.startsWith("opencode.throw")) throw new Error("storage get failed") return this.values.get(key) ?? null } setItem(key: string, value: string) { this.calls.set += 1 this.events.push(`set:${key}`) if (key.startsWith("opencode.quota")) throw new DOMException("quota", "QuotaExceededError") if (key.startsWith("opencode.throw")) throw new Error("storage set failed") this.values.set(key, value) } removeItem(key: string) { this.calls.remove += 1 this.events.push(`remove:${key}`) if (key.startsWith("opencode.throw")) throw new Error("storage remove failed") this.values.delete(key) } } const storage = new MemoryStorage() let persistTesting: PersistTestingType let Persist: PersistType let removePersisted: RemovePersistedType beforeAll(async () => { mock.module("@/context/platform", () => ({ usePlatform: () => ({ platform: "web" }), })) const mod = await import("./persist") persistTesting = mod.PersistTesting Persist = mod.Persist removePersisted = mod.removePersisted }) beforeEach(() => { storage.clear() storage.events.length = 0 storage.calls.get = 0 storage.calls.set = 0 storage.calls.remove = 0 Object.defineProperty(globalThis, "localStorage", { value: storage, configurable: true, }) }) describe("persist localStorage resilience", () => { test("does not cache values as persisted when quota write and eviction fail", () => { const storageApi = persistTesting.localStorageWithPrefix("opencode.quota.scope") storageApi.setItem("value", '{"value":1}') expect(storage.getItem("opencode.quota.scope:value")).toBeNull() expect(storageApi.getItem("value")).toBeNull() }) test("disables only the failing scope when storage throws", () => { const bad = persistTesting.localStorageWithPrefix("opencode.throw.scope") bad.setItem("value", '{"value":1}') const before = storage.calls.set bad.setItem("value", '{"value":2}') expect(storage.calls.set).toBe(before) expect(bad.getItem("value")).toBeNull() const healthy = persistTesting.localStorageWithPrefix("opencode.safe.scope") healthy.setItem("value", '{"value":3}') expect(storage.getItem("opencode.safe.scope:value")).toBe('{"value":3}') }) test("failing fallback scope does not poison direct storage scope", () => { const broken = persistTesting.localStorageWithPrefix("opencode.throw.scope2") broken.setItem("value", '{"value":1}') const direct = persistTesting.localStorageDirect() direct.setItem("direct-value", '{"value":5}') expect(storage.getItem("direct-value")).toBe('{"value":5}') }) test("normalizer rejects malformed JSON payloads", () => { const result = persistTesting.normalize({ value: "ok" }, '{"value":"\\x"}') expect(result).toBeUndefined() }) test("workspace storage sanitizes Windows filename characters", () => { const result = persistTesting.workspaceStorage("C:\\Users\\foo") expect(result).toStartWith("opencode.workspace.") expect(result.endsWith(".dat")).toBeTrue() expect(/[:\\/]/.test(result)).toBeFalse() }) test("workspace target keeps raw path storage as legacy fallback", () => { const target = Persist.workspace("C:\\Users\\foo", "vcs") expect(target.storage).toBe(persistTesting.workspaceStorage("C:/Users/foo")) expect(target.legacyStorageNames).toEqual([persistTesting.workspaceStorage("C:\\Users\\foo")]) }) test("workspace target keeps backslash storage as fallback for normalized Windows paths", () => { const target = Persist.workspace("C:/Users/foo", "vcs") expect(target.storage).toBe(persistTesting.workspaceStorage("C:/Users/foo")) expect(target.legacyStorageNames).toEqual([persistTesting.workspaceStorage("C:\\Users\\foo")]) }) test("migrates direct legacy keys into scoped storage", () => { storage.setItem("legacy.workspace", '{"value":2}') const target = Persist.workspace("C:/Users/foo", "demo", ["legacy.workspace"]) const current = persistTesting.localStorageWithPrefix(target.storage!) const legacyStore = persistTesting.localStorageDirect() const result = persistTesting.migrateLegacy({ current, legacyStore, stores: [], keys: target.legacy!, key: target.key, defaults: { value: 1 }, }) expect(result).toBe('{"value":2}') expect(storage.getItem(`${target.storage}:${target.key}`)).toBe('{"value":2}') expect(legacyStore.getItem("legacy.workspace")).toBeNull() expect(storage.getItem("legacy.workspace")).toBeNull() }) test("removes legacy workspace storage when removing persisted target", () => { const target = Persist.workspace("C:\\Users\\foo", "terminal") storage.setItem(`${target.storage}:${target.key}`, '{"value":1}') storage.setItem(`${target.legacyStorageNames![0]}:${target.key}`, '{"value":2}') removePersisted(target) expect(storage.getItem(`${target.storage}:${target.key}`)).toBeNull() expect(storage.getItem(`${target.legacyStorageNames![0]}:${target.key}`)).toBeNull() }) })