import { afterEach, expect, test } from "bun:test"; import { appendFile, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { grow } from "../src/optchat-core.js"; import { Memory } from "../src/optchat-memory.js"; const directories: string[] = []; afterEach(async () => { await Promise.all( directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), ); }); async function fixture() { const directory = await mkdtemp(join(tmpdir(), "optchat-memory-")); directories.push(directory); return join(directory, "ses_root"); } test("logs append-only, dedupe by source, split long texts, and reload with tree and view", async () => { const directory = await fixture(); const memory = await Memory.open(directory); expect(memory.log("user", "hello", "user:m1:0")).toEqual([0]); expect(memory.log("user", "hello", "user:m1:0")).toEqual([]); expect(memory.log("agent", "x".repeat(70_000), "assistant:m2:0")).toEqual([1, 2, 3]); memory.node([0, 0], "user: hello"); let total = 0; for (const message of memory.messages) total = grow(memory.view, message.i, (line) => memory.text(line), total, true).total; memory.saveView(); await memory.close(); const [day] = await readdir(join(directory, "main")); const lines = (await readFile(join(directory, "main", day!), "utf8")).trim().split("\n"); expect(lines.map((line) => JSON.parse(line).i)).toEqual([0, 1, 2, 3]); expect(JSON.parse(lines[1]!).size).toBe(30_000); // A torn line from a crash is skipped. await appendFile(join(directory, "main", day!), '{"i":4,"kind":"us'); const again = await Memory.open(directory); expect(again.messages.map((message) => message.text.length)).toEqual([ 5, 30_000, 30_000, 10_000, ]); expect(again.has("assistant:m2:0")).toBe(true); expect(again.has("assistant:m2:0#2")).toBe(true); expect(again.text([0, 0])).toBe("user: hello"); expect(again.nodes()).toEqual([[0, 0]]); expect(again.view.lines).toEqual([ [0, 0], [0, 1], [0, 2], [0, 3], ]); expect(again.log("user", "next", "user:m3:0")).toEqual([4]); await again.close(); }); test("one writer: the lock rejects a live holder and replaces a dead one", async () => { const directory = await fixture(); const memory = await Memory.open(directory); await expect(Memory.open(directory)).rejects.toThrow("already open"); await memory.close(); const live = Bun.spawn(["sleep", "30"]); try { await writeFile(join(directory, "lock"), String(live.pid)); await expect(Memory.open(directory)).rejects.toThrow(`in use by process ${live.pid}`); } finally { live.kill(); await live.exited; } const again = await Memory.open(directory); expect(await readFile(join(directory, "lock"), "utf8")).toBe(String(process.pid)); await again.close(); });