@@ -0,0 +1,90 @@
|
||||
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("messages logged after the saved view are appended to it at load", async () => {
|
||||
const directory = await fixture();
|
||||
const memory = await Memory.open(directory);
|
||||
memory.log("user", "a", "s:0");
|
||||
memory.saveView();
|
||||
memory.log("user", "b", "s:1");
|
||||
await memory.close();
|
||||
const again = await Memory.open(directory);
|
||||
expect(again.view.lines).toEqual([
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
]);
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user