76 lines
3.0 KiB
TypeScript
76 lines
3.0 KiB
TypeScript
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();
|
|
});
|