import { describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" import { Effect, Layer, ManagedRuntime } from "effect" import { AppFileSystem } from "../../src/filesystem" import { Global } from "../../src/global" import { Storage } from "../../src/storage/storage" import { tmpdir } from "../fixture/fixture" const dir = path.join(Global.Path.data, "storage") async function withScope(fn: (root: string[]) => Promise) { const root = ["storage_test", crypto.randomUUID()] try { return await fn(root) } finally { await fs.rm(path.join(dir, ...root), { recursive: true, force: true }) } } function map(root: string, file: string) { if (file === Global.Path.data) return root if (file.startsWith(Global.Path.data + path.sep)) return path.join(root, path.relative(Global.Path.data, file)) return file } function layer(root: string) { return Layer.effect( AppFileSystem.Service, Effect.gen(function* () { const fs = yield* AppFileSystem.Service return AppFileSystem.Service.of({ ...fs, isDir: (file) => fs.isDir(map(root, file)), readJson: (file) => fs.readJson(map(root, file)), writeWithDirs: (file, content, mode) => fs.writeWithDirs(map(root, file), content, mode), readFileString: (file) => fs.readFileString(map(root, file)), remove: (file) => fs.remove(map(root, file)), glob: (pattern, options) => fs.glob(pattern, options?.cwd ? { ...options, cwd: map(root, options.cwd) } : options), }) }), ).pipe(Layer.provide(AppFileSystem.defaultLayer)) } async function withStorage( root: string, fn: (run: (body: Effect.Effect) => Promise) => Promise, ) { const rt = ManagedRuntime.make(Storage.layer.pipe(Layer.provide(layer(root)))) try { return await fn((body) => rt.runPromise(body)) } finally { await rt.dispose() } } async function write(file: string, value: unknown) { await fs.mkdir(path.dirname(file), { recursive: true }) await Bun.write(file, JSON.stringify(value, null, 2)) } async function text(file: string, value: string) { await fs.mkdir(path.dirname(file), { recursive: true }) await Bun.write(file, value) } async function exists(file: string) { return fs .stat(file) .then(() => true) .catch(() => false) } describe("Storage", () => { test("round-trips JSON content", async () => { await withScope(async (root) => { const key = [...root, "session_diff", "roundtrip"] const value = [{ file: "a.ts", additions: 2, deletions: 1 }] await Storage.write(key, value) expect(await Storage.read(key)).toEqual(value) }) }) test("maps missing reads to NotFoundError", async () => { await withScope(async (root) => { await expect(Storage.read([...root, "missing", "value"])).rejects.toMatchObject({ name: "NotFoundError" }) }) }) test("update on missing key throws NotFoundError", async () => { await withScope(async (root) => { await expect( Storage.update<{ value: number }>([...root, "missing", "key"], (draft) => { draft.value += 1 }), ).rejects.toMatchObject({ name: "NotFoundError" }) }) }) test("write overwrites existing value", async () => { await withScope(async (root) => { const key = [...root, "overwrite", "test"] await Storage.write<{ v: number }>(key, { v: 1 }) await Storage.write<{ v: number }>(key, { v: 2 }) expect(await Storage.read<{ v: number }>(key)).toEqual({ v: 2 }) }) }) test("remove on missing key is a no-op", async () => { await withScope(async (root) => { await expect(Storage.remove([...root, "nonexistent", "key"])).resolves.toBeUndefined() }) }) test("list on missing prefix returns empty", async () => { await withScope(async (root) => { expect(await Storage.list([...root, "nonexistent"])).toEqual([]) }) }) test("serializes concurrent updates for the same key", async () => { await withScope(async (root) => { const key = [...root, "counter", "shared"] await Storage.write(key, { value: 0 }) await Promise.all( Array.from({ length: 25 }, () => Storage.update<{ value: number }>(key, (draft) => { draft.value += 1 }), ), ) expect(await Storage.read<{ value: number }>(key)).toEqual({ value: 25 }) }) }) test("concurrent reads do not block each other", async () => { await withScope(async (root) => { const key = [...root, "concurrent", "reads"] await Storage.write(key, { ok: true }) const results = await Promise.all(Array.from({ length: 10 }, () => Storage.read(key))) expect(results).toHaveLength(10) for (const r of results) expect(r).toEqual({ ok: true }) }) }) test("nested keys create deep paths", async () => { await withScope(async (root) => { const key = [...root, "a", "b", "c", "deep"] await Storage.write<{ nested: boolean }>(key, { nested: true }) expect(await Storage.read<{ nested: boolean }>(key)).toEqual({ nested: true }) expect(await Storage.list([...root, "a"])).toEqual([key]) }) }) test("lists and removes stored entries", async () => { await withScope(async (root) => { const a = [...root, "list", "a"] const b = [...root, "list", "b"] const prefix = [...root, "list"] await Storage.write(b, { value: 2 }) await Storage.write(a, { value: 1 }) expect(await Storage.list(prefix)).toEqual([a, b]) await Storage.remove(a) expect(await Storage.list(prefix)).toEqual([b]) await expect(Storage.read(a)).rejects.toMatchObject({ name: "NotFoundError" }) }) }) test("migration 2 runs when marker contents are invalid", async () => { await using tmp = await tmpdir() const storage = path.join(tmp.path, "storage") const diffs = [ { additions: 2, deletions: 1 }, { additions: 3, deletions: 4 }, ] await text(path.join(storage, "migration"), "wat") await write(path.join(storage, "session", "proj_test", "ses_test.json"), { id: "ses_test", projectID: "proj_test", title: "legacy", summary: { diffs }, }) await withStorage(tmp.path, async (run) => { expect(await run(Storage.Service.use((svc) => svc.list(["session_diff"])))).toEqual([ ["session_diff", "ses_test"], ]) expect(await run(Storage.Service.use((svc) => svc.read(["session_diff", "ses_test"])))).toEqual( diffs, ) expect( await run( Storage.Service.use((svc) => svc.read<{ id: string projectID: string title: string summary: { additions: number deletions: number } }>(["session", "proj_test", "ses_test"]), ), ), ).toEqual({ id: "ses_test", projectID: "proj_test", title: "legacy", summary: { additions: 5, deletions: 5, }, }) }) expect(await Bun.file(path.join(storage, "migration")).text()).toBe("2") }) test("migration 1 tolerates malformed legacy records", async () => { await using tmp = await tmpdir({ git: true }) const storage = path.join(tmp.path, "storage") const legacy = path.join(tmp.path, "project", "legacy") await write(path.join(legacy, "storage", "session", "message", "probe", "0.json"), []) await write(path.join(legacy, "storage", "session", "message", "probe", "1.json"), { path: { root: tmp.path }, }) await write(path.join(legacy, "storage", "session", "info", "ses_legacy.json"), { id: "ses_legacy", title: "legacy", }) await write(path.join(legacy, "storage", "session", "message", "ses_legacy", "msg_legacy.json"), { role: "user", text: "hello", }) await withStorage(tmp.path, async (run) => { const projects = await run(Storage.Service.use((svc) => svc.list(["project"]))) expect(projects).toHaveLength(1) const project = projects[0]![1] expect(await run(Storage.Service.use((svc) => svc.list(["session", project])))).toEqual([ ["session", project, "ses_legacy"], ]) expect( await run( Storage.Service.use((svc) => svc.read<{ id: string; title: string }>(["session", project, "ses_legacy"])), ), ).toEqual({ id: "ses_legacy", title: "legacy", }) expect( await run( Storage.Service.use((svc) => svc.read<{ role: string; text: string }>(["message", "ses_legacy", "msg_legacy"]), ), ), ).toEqual({ role: "user", text: "hello", }) }) expect(await Bun.file(path.join(storage, "migration")).text()).toBe("2") }) test("failed migrations do not advance the marker", async () => { await using tmp = await tmpdir() const storage = path.join(tmp.path, "storage") const legacy = path.join(tmp.path, "project", "legacy") await text(path.join(legacy, "storage", "session", "message", "probe", "0.json"), "{") await withStorage(tmp.path, async (run) => { expect(await run(Storage.Service.use((svc) => svc.list(["project"])))).toEqual([]) }) expect(await exists(path.join(storage, "migration"))).toBe(false) }) })