mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-03 04:36:26 +00:00
285 lines
10 KiB
TypeScript
285 lines
10 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { Schema, SchemaGetter } from "effect"
|
|
import { createComputed, createRoot } from "solid-js"
|
|
import type { Platform } from "@/runtime/platform/platform"
|
|
import { flushPersisted } from "@/runtime/persistence/persist"
|
|
import { Persist, persisted } from "@/runtime/persistence/storage"
|
|
import { Persistence } from "@/runtime/persistence/schema"
|
|
import { TabStorage } from "@/shell/tabs/schema"
|
|
|
|
const Current = Schema.Struct({
|
|
enabled: Schema.Boolean,
|
|
label: Schema.String,
|
|
})
|
|
|
|
const initial = { enabled: true, label: "default" }
|
|
|
|
const Stored = Persistence.migrate(
|
|
Current,
|
|
Persistence.legacy({ oldLabel: Schema.optional(Schema.String), label: Schema.optional(Schema.String) }).pipe(
|
|
Schema.decode({
|
|
decode: SchemaGetter.transform((value) =>
|
|
value.oldLabel === undefined ? value : { ...value, label: value.oldLabel },
|
|
),
|
|
encode: SchemaGetter.passthrough(),
|
|
}),
|
|
),
|
|
)
|
|
|
|
const web: Platform = {
|
|
platform: "web",
|
|
openExternal: () => undefined,
|
|
restart: async () => undefined,
|
|
notify: async () => undefined,
|
|
}
|
|
|
|
function desktop() {
|
|
const values = new Map<string, string>()
|
|
|
|
const platform: Platform = {
|
|
...web,
|
|
platform: "desktop",
|
|
windowID: "schema-test",
|
|
openDirectoryPickerDialog: async () => null,
|
|
storage: (name) => ({
|
|
getItem: async (key) => values.get(`${name}:${key}`) ?? null,
|
|
setItem: async (key, value) => void values.set(`${name}:${key}`, value),
|
|
removeItem: async (key) => void values.delete(`${name}:${key}`),
|
|
}),
|
|
}
|
|
|
|
return { values, platform }
|
|
}
|
|
|
|
describe("schema-backed persistence", () => {
|
|
test("clears the recent tab after restoring it from storage", () => {
|
|
const target = Persist.global("schema-recent-clear")
|
|
const key = `${target.storage}:${target.key}`
|
|
localStorage.setItem(key, JSON.stringify({ key: "session-tab" }))
|
|
createRoot((dispose) => {
|
|
const [state, setState] = persisted(target, TabStorage.Recent, { key: undefined }, web)
|
|
|
|
try {
|
|
expect(state.key).toBe("session-tab")
|
|
setState("key", undefined)
|
|
expect(state.key).toBeUndefined()
|
|
flushPersisted()
|
|
expect(localStorage.getItem(key)).toBe("{}")
|
|
} finally {
|
|
dispose()
|
|
}
|
|
})
|
|
})
|
|
|
|
test("migrates sync storage and writes only the current representation", () => {
|
|
const target = Persist.global("schema-sync")
|
|
const key = `${target.storage}:${target.key}`
|
|
localStorage.setItem(key, JSON.stringify({ oldLabel: "saved" }))
|
|
createRoot((dispose) => {
|
|
const [state, setState, , ready] = persisted(target, Stored, initial, web)
|
|
expect(ready.promise).toBeUndefined()
|
|
expect(state).toEqual({ enabled: true, label: "saved" })
|
|
expect(JSON.parse(localStorage.getItem(key)!)).toEqual({ enabled: true, label: "saved" })
|
|
setState("enabled", false)
|
|
flushPersisted()
|
|
expect(JSON.parse(localStorage.getItem(key)!)).toEqual({ enabled: false, label: "saved" })
|
|
dispose()
|
|
})
|
|
})
|
|
|
|
test("keeps valid current fields that the migration reader does not name", () => {
|
|
const target = Persist.global("schema-unnamed-field")
|
|
const key = `${target.storage}:${target.key}`
|
|
localStorage.setItem(key, JSON.stringify({ enabled: false, label: "kept" }))
|
|
createRoot((dispose) => {
|
|
const [state] = persisted(target, Stored, initial, web)
|
|
expect(state).toEqual({ enabled: false, label: "kept" })
|
|
expect(JSON.parse(localStorage.getItem(key)!)).toEqual({ enabled: false, label: "kept" })
|
|
dispose()
|
|
})
|
|
})
|
|
|
|
test("recovers invalid fields and strips fields outside the schema", () => {
|
|
const target = Persist.global("schema-invalid-field")
|
|
localStorage.setItem(
|
|
`${target.storage}:${target.key}`,
|
|
JSON.stringify({ enabled: "false", label: "kept", extra: 1 }),
|
|
)
|
|
createRoot((dispose) => {
|
|
const [state] = persisted(target, Stored, initial, web)
|
|
expect(state).toEqual({ enabled: true, label: "kept" })
|
|
dispose()
|
|
})
|
|
})
|
|
|
|
test("malformed JSON falls back to a typed initial state", () => {
|
|
const target = Persist.global("schema-invalid-json")
|
|
localStorage.setItem(`${target.storage}:${target.key}`, '{"label":"\\x"}')
|
|
createRoot((dispose) => {
|
|
const [state] = persisted(target, Stored, { enabled: false, label: "initial" }, web)
|
|
expect(state).toEqual({ enabled: false, label: "initial" })
|
|
expect(localStorage.getItem(`${target.storage}:${target.key}`)).toBeNull()
|
|
dispose()
|
|
})
|
|
})
|
|
|
|
test("relocates and canonicalizes desktop state before becoming ready", async () => {
|
|
const storage = desktop()
|
|
storage.values.set("undefined:old-schema", JSON.stringify({ oldLabel: "desktop" }))
|
|
|
|
const root = createRoot((dispose) => ({
|
|
dispose,
|
|
state: persisted(
|
|
{ ...Persist.global("schema-desktop"), previousKey: "old-schema" },
|
|
Stored,
|
|
initial,
|
|
storage.platform,
|
|
),
|
|
}))
|
|
|
|
try {
|
|
expect(root.state[3]()).toBe(false)
|
|
await root.state[3].promise
|
|
expect(root.state[0]).toEqual({ enabled: true, label: "desktop" })
|
|
expect(storage.values.has("undefined:old-schema")).toBe(false)
|
|
expect(JSON.parse(storage.values.get("opencode.global.dat:schema-desktop")!)).toEqual({
|
|
enabled: true,
|
|
label: "desktop",
|
|
})
|
|
root.state[1]("label", "changed")
|
|
flushPersisted()
|
|
expect(JSON.parse(storage.values.get("opencode.global.dat:schema-desktop")!)).toEqual({
|
|
enabled: true,
|
|
label: "changed",
|
|
})
|
|
} finally {
|
|
root.dispose()
|
|
}
|
|
})
|
|
|
|
test("a late desktop read does not overwrite an edit made while loading", async () => {
|
|
const pending = Promise.withResolvers<string | null>()
|
|
const storage = desktop()
|
|
storage.platform.storage = () => ({
|
|
getItem: () => pending.promise,
|
|
setItem: async () => undefined,
|
|
removeItem: async () => undefined,
|
|
})
|
|
|
|
const root = createRoot((dispose) => ({
|
|
dispose,
|
|
state: persisted(Persist.global("schema-late"), Stored, initial, storage.platform),
|
|
}))
|
|
|
|
try {
|
|
root.state[1]("label", "new edit")
|
|
pending.resolve(JSON.stringify({ oldLabel: "old state" }))
|
|
await root.state[3].promise
|
|
expect(root.state[0].label).toBe("new edit")
|
|
} finally {
|
|
root.dispose()
|
|
}
|
|
})
|
|
|
|
test("a full or failing storage scope stops writing without disabling other scopes", () => {
|
|
const full = Persist.workspace("/schema-storage-full", "state")
|
|
const failing = Persist.workspace("/schema-storage-failing", "state")
|
|
const healthy = Persist.workspace("/schema-storage-healthy", "state")
|
|
const direct = "schema-storage-direct"
|
|
const values = new Map<string, string>()
|
|
const attempts: string[] = []
|
|
|
|
const storage: Storage = {
|
|
get length() {
|
|
return values.size
|
|
},
|
|
key: (index) => [...values.keys()][index] ?? null,
|
|
clear: () => values.clear(),
|
|
getItem: (key) => values.get(key) ?? null,
|
|
setItem: (key, value) => {
|
|
attempts.push(key)
|
|
|
|
if (key.startsWith(`${full.storage}:`)) throw new DOMException("quota", "QuotaExceededError")
|
|
|
|
if (key.startsWith(`${failing.storage}:`)) throw new Error("storage set failed")
|
|
values.set(key, value)
|
|
},
|
|
removeItem: (key) => void values.delete(key),
|
|
}
|
|
|
|
const original = Object.getOwnPropertyDescriptor(globalThis, "localStorage")!
|
|
Object.defineProperty(globalThis, "localStorage", { value: storage, configurable: true })
|
|
|
|
try {
|
|
createRoot((dispose) => {
|
|
const [, setFull] = persisted(full, Current, initial, web)
|
|
const [, setFailing] = persisted(failing, Current, initial, web)
|
|
|
|
const count = () => ({
|
|
full: attempts.filter((key) => key === `${full.storage}:${full.key}`).length,
|
|
failing: attempts.filter((key) => key === `${failing.storage}:${failing.key}`).length,
|
|
})
|
|
|
|
setFull("label", "full")
|
|
setFailing("label", "first")
|
|
flushPersisted()
|
|
const first = count()
|
|
expect(first.full).toBeGreaterThan(0)
|
|
expect(first.failing).toBeGreaterThan(0)
|
|
setFull("label", "full again")
|
|
setFailing("label", "second")
|
|
flushPersisted()
|
|
expect(count()).toEqual(first)
|
|
|
|
const [, setHealthy] = persisted(healthy, Current, initial, web)
|
|
const [, setDirect] = persisted(direct, Current, initial, web)
|
|
setHealthy("label", "healthy")
|
|
setDirect("label", "direct")
|
|
flushPersisted()
|
|
expect(values.get(`${healthy.storage}:${healthy.key}`)).toBe(
|
|
JSON.stringify({ enabled: true, label: "healthy" }),
|
|
)
|
|
expect(values.get(direct)).toBe(JSON.stringify({ enabled: true, label: "direct" }))
|
|
dispose()
|
|
})
|
|
expect(values.has(`${full.storage}:${full.key}`)).toBe(false)
|
|
createRoot((dispose) => {
|
|
const [state] = persisted(full, Current, initial, web)
|
|
expect(state).toEqual(initial)
|
|
dispose()
|
|
})
|
|
} finally {
|
|
Object.defineProperty(globalThis, "localStorage", original)
|
|
}
|
|
})
|
|
|
|
test("cross-window updates use the same migration and validation boundary", async () => {
|
|
const target = { ...Persist.global("schema-sync-channel"), sync: true }
|
|
const channel = new BroadcastChannel(`opencode.persist:${target.storage}:${target.key}`)
|
|
const received = Promise.withResolvers<void>()
|
|
const values: unknown[] = []
|
|
|
|
const root = createRoot((dispose) => {
|
|
const [state] = persisted(target, Stored, initial, web)
|
|
createComputed(() => {
|
|
values.push({ enabled: state.enabled, label: state.label })
|
|
|
|
if (state.label === "from another window") received.resolve()
|
|
})
|
|
|
|
return { dispose, state }
|
|
})
|
|
|
|
try {
|
|
channel.postMessage({ key: target.key, newValue: JSON.stringify({ enabled: "false", label: "recovered" }) })
|
|
channel.postMessage({ key: target.key, newValue: JSON.stringify({ oldLabel: "from another window" }) })
|
|
await received.promise
|
|
expect(root.state).toEqual({ enabled: true, label: "from another window" })
|
|
expect(values).toContainEqual({ enabled: true, label: "recovered" })
|
|
expect(values).toContainEqual({ enabled: true, label: "from another window" })
|
|
} finally {
|
|
channel.close()
|
|
root.dispose()
|
|
}
|
|
})
|
|
})
|