Thread tracking, decision extraction, boot context generation, pre-compaction snapshots, structured narratives. - 10 source files, 1983 LOC TypeScript - 9 test files, 270 tests passing - Zero runtime dependencies - Cerberus approved + all findings fixed - EN/DE pattern matching, atomic file writes - Graceful degradation (read-only workspace, corrupt JSON)
184 lines
5 KiB
TypeScript
184 lines
5 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import {
|
|
loadJson,
|
|
saveJson,
|
|
loadText,
|
|
saveText,
|
|
rebootDir,
|
|
ensureRebootDir,
|
|
isWritable,
|
|
getFileMtime,
|
|
isFileOlderThan,
|
|
} from "../src/storage.js";
|
|
|
|
const logger = {
|
|
info: () => {},
|
|
warn: () => {},
|
|
error: () => {},
|
|
debug: () => {},
|
|
};
|
|
|
|
function makeTmp(): string {
|
|
return mkdtempSync(join(tmpdir(), "cortex-storage-"));
|
|
}
|
|
|
|
describe("rebootDir", () => {
|
|
it("returns memory/reboot path", () => {
|
|
expect(rebootDir("/workspace")).toBe(join("/workspace", "memory", "reboot"));
|
|
});
|
|
});
|
|
|
|
describe("ensureRebootDir", () => {
|
|
it("creates the directory", () => {
|
|
const ws = makeTmp();
|
|
const ok = ensureRebootDir(ws, logger);
|
|
expect(ok).toBe(true);
|
|
const stat = require("node:fs").statSync(rebootDir(ws));
|
|
expect(stat.isDirectory()).toBe(true);
|
|
});
|
|
|
|
it("returns true if directory already exists", () => {
|
|
const ws = makeTmp();
|
|
mkdirSync(join(ws, "memory", "reboot"), { recursive: true });
|
|
expect(ensureRebootDir(ws, logger)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("isWritable", () => {
|
|
it("returns true for writable workspace", () => {
|
|
const ws = makeTmp();
|
|
expect(isWritable(ws)).toBe(true);
|
|
});
|
|
|
|
it("returns true when memory/ dir exists and is writable", () => {
|
|
const ws = makeTmp();
|
|
mkdirSync(join(ws, "memory"), { recursive: true });
|
|
expect(isWritable(ws)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("loadJson", () => {
|
|
it("loads valid JSON", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "test.json");
|
|
writeFileSync(f, '{"a":1}');
|
|
const result = loadJson<{ a: number }>(f);
|
|
expect(result.a).toBe(1);
|
|
});
|
|
|
|
it("returns empty object for missing file", () => {
|
|
const result = loadJson("/nonexistent/path.json");
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it("returns empty object for corrupt JSON", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "bad.json");
|
|
writeFileSync(f, "not json {{{");
|
|
expect(loadJson(f)).toEqual({});
|
|
});
|
|
|
|
it("returns empty object for empty file", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "empty.json");
|
|
writeFileSync(f, "");
|
|
expect(loadJson(f)).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("saveJson", () => {
|
|
it("writes valid JSON atomically", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "out.json");
|
|
const ok = saveJson(f, { hello: "world" }, logger);
|
|
expect(ok).toBe(true);
|
|
const content = JSON.parse(readFileSync(f, "utf-8"));
|
|
expect(content.hello).toBe("world");
|
|
});
|
|
|
|
it("creates parent directories", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "sub", "deep", "out.json");
|
|
const ok = saveJson(f, { nested: true }, logger);
|
|
expect(ok).toBe(true);
|
|
expect(JSON.parse(readFileSync(f, "utf-8")).nested).toBe(true);
|
|
});
|
|
|
|
it("no .tmp file left after successful write", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "clean.json");
|
|
saveJson(f, { clean: true }, logger);
|
|
const fs = require("node:fs");
|
|
expect(fs.existsSync(f + ".tmp")).toBe(false);
|
|
});
|
|
|
|
it("pretty-prints with 2-space indent", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "pretty.json");
|
|
saveJson(f, { a: 1 }, logger);
|
|
const raw = readFileSync(f, "utf-8");
|
|
expect(raw).toContain(" ");
|
|
expect(raw.endsWith("\n")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("loadText", () => {
|
|
it("loads text file content", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "note.md");
|
|
writeFileSync(f, "# Hello\nWorld");
|
|
expect(loadText(f)).toBe("# Hello\nWorld");
|
|
});
|
|
|
|
it("returns empty string for missing file", () => {
|
|
expect(loadText("/nonexistent/file.md")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("saveText", () => {
|
|
it("writes text file atomically", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "out.md");
|
|
const ok = saveText(f, "# Test", logger);
|
|
expect(ok).toBe(true);
|
|
expect(readFileSync(f, "utf-8")).toBe("# Test");
|
|
});
|
|
|
|
it("creates parent directories", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "a", "b", "out.md");
|
|
saveText(f, "deep", logger);
|
|
expect(readFileSync(f, "utf-8")).toBe("deep");
|
|
});
|
|
});
|
|
|
|
describe("getFileMtime", () => {
|
|
it("returns ISO string for existing file", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "file.txt");
|
|
writeFileSync(f, "x");
|
|
const mtime = getFileMtime(f);
|
|
expect(mtime).toBeTruthy();
|
|
expect(new Date(mtime!).getTime()).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("returns null for missing file", () => {
|
|
expect(getFileMtime("/nonexistent")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("isFileOlderThan", () => {
|
|
it("returns true for missing file", () => {
|
|
expect(isFileOlderThan("/nonexistent", 1)).toBe(true);
|
|
});
|
|
|
|
it("returns false for fresh file", () => {
|
|
const ws = makeTmp();
|
|
const f = join(ws, "fresh.txt");
|
|
writeFileSync(f, "new");
|
|
expect(isFileOlderThan(f, 1)).toBe(false);
|
|
});
|
|
});
|