- Implemented MemoryStore class for managing concepts with YAML frontmatter. - Added methods for creating, reading, updating, and deleting concepts. - Introduced locking mechanism for concurrent access. - Developed search and hinting capabilities for concept retrieval. - Created tests for access control and store functionality. - Added long-term memory skill documentation and operational guidelines. - Included package.json and package-lock.json for dependency management.
152 lines
6.1 KiB
JavaScript
152 lines
6.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, readFile, readdir, rm, utimes, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
import { MemoryStore, parseFrontmatter } from "../store.mjs";
|
|
|
|
async function fixture(options = {}) {
|
|
const root = await mkdtemp(join(tmpdir(), "kimiko-memory-test-"));
|
|
const store = new MemoryStore(root, options);
|
|
await store.initialize();
|
|
return { root, store };
|
|
}
|
|
|
|
async function cleanup(root) {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
|
|
test("writes valid concepts, preserves timestamps on no-op, and updates generated files", async () => {
|
|
const dates = [new Date("2026-07-22T09:30:00.000Z"), new Date("2026-07-23T10:00:00.000Z")];
|
|
let dateIndex = 0;
|
|
const { root, store } = await fixture({ clock: () => dates[Math.min(dateIndex, dates.length - 1)] });
|
|
try {
|
|
const created = await store.upsert({
|
|
id: "people/alice",
|
|
type: "Person",
|
|
title: "Alice",
|
|
description: "A test person.",
|
|
tags: ["test", "test"],
|
|
body: "# Facts\n\n- Likes deterministic systems.",
|
|
});
|
|
assert.equal(created.action, "created");
|
|
assert.equal(created.timestamp, dates[0].toISOString());
|
|
|
|
dateIndex = 1;
|
|
const unchanged = await store.upsert({
|
|
id: "people/alice",
|
|
type: "Person",
|
|
title: "Alice",
|
|
description: "A test person.",
|
|
tags: ["test"],
|
|
body: "# Facts\n\n- Likes deterministic systems.",
|
|
});
|
|
assert.equal(unchanged.action, "unchanged");
|
|
assert.equal(unchanged.timestamp, dates[0].toISOString());
|
|
|
|
const parsed = parseFrontmatter(await readFile(join(root, "people", "alice.md"), "utf8"));
|
|
assert.equal(parsed.metadata.type, "Person");
|
|
assert.equal(parsed.metadata.timestamp, dates[0].toISOString());
|
|
assert.deepEqual(parsed.metadata.tags, ["test"]);
|
|
|
|
const rootIndex = await readFile(join(root, "index.md"), "utf8");
|
|
const peopleIndex = await readFile(join(root, "people", "index.md"), "utf8");
|
|
const log = await readFile(join(root, "log.md"), "utf8");
|
|
assert.match(rootIndex, /okf_version: "?0\.1"?/);
|
|
assert.match(rootIndex, /\[People\]\(people\/\)/);
|
|
assert.match(peopleIndex, /\[Alice\]\(alice\.md\) - A test person\./);
|
|
assert.match(log, /\*\*Creation\*\*/);
|
|
} finally {
|
|
await cleanup(root);
|
|
}
|
|
});
|
|
|
|
test("verified writes retry once after deterministic verification failure", async () => {
|
|
let injected = false;
|
|
const { root, store } = await fixture({
|
|
clock: () => new Date("2026-07-22T09:30:00.000Z"),
|
|
afterWriteAttempt: async ({ path, attempt, kind }) => {
|
|
if (!injected && kind === "concept" && attempt === 1 && path.endsWith("retry.md")) {
|
|
injected = true;
|
|
await writeFile(path, "broken", "utf8");
|
|
}
|
|
},
|
|
});
|
|
try {
|
|
const result = await store.upsert({ id: "retry", type: "Reference", body: "# Retried" });
|
|
assert.equal(result.attempts, 2);
|
|
assert.equal(parseFrontmatter(await readFile(join(root, "retry.md"), "utf8")).metadata.type, "Reference");
|
|
} finally {
|
|
await cleanup(root);
|
|
}
|
|
});
|
|
|
|
test("audit repairs missing and malformed frontmatter without discarding bodies", async () => {
|
|
const { root, store } = await fixture({ clock: () => new Date("2026-07-22T09:30:00.000Z") });
|
|
try {
|
|
const missingPath = join(root, "missing.md");
|
|
const malformedPath = join(root, "malformed.md");
|
|
await writeFile(missingPath, "# Original\n\nKeep this body.\n", "utf8");
|
|
await writeFile(malformedPath, "---\ntype: [broken\n---\n\n# Also Original\n", "utf8");
|
|
const mtime = new Date("2026-01-02T03:04:05.000Z");
|
|
await utimes(missingPath, mtime, mtime);
|
|
|
|
const report = await store.audit({ repair: true });
|
|
assert.equal(report.repaired, 2);
|
|
assert.equal(report.valid, true);
|
|
|
|
const missing = parseFrontmatter(await readFile(missingPath, "utf8"));
|
|
const malformed = parseFrontmatter(await readFile(malformedPath, "utf8"));
|
|
assert.equal(missing.metadata.type, "Recovered Concept");
|
|
assert.equal(missing.metadata.timestamp, mtime.toISOString());
|
|
assert.match(missing.body, /Keep this body/);
|
|
assert.match(malformed.body, /Also Original/);
|
|
assert.ok(malformed.metadata.repair_backup);
|
|
assert.ok((await readdir(join(root, ".recovery"))).length >= 1);
|
|
} finally {
|
|
await cleanup(root);
|
|
}
|
|
});
|
|
|
|
test("search is metadata-first and forget requires an exact confirmation", async () => {
|
|
const { root, store } = await fixture({ clock: () => new Date("2026-07-22T09:30:00.000Z") });
|
|
try {
|
|
await store.upsert({
|
|
id: "projects/atlas",
|
|
type: "Project",
|
|
title: "Atlas",
|
|
description: "Infrastructure provisioning platform.",
|
|
tags: ["platform"],
|
|
body: "# Secret Detail\n\nBody-only retrieval token: zephyrquartz.",
|
|
});
|
|
const found = await store.search("infrastructure atlas", 5);
|
|
assert.equal(found.results[0].id, "projects/atlas");
|
|
assert.equal("body" in found.results[0], false);
|
|
|
|
assert.equal((await store.hints("What did we decide about Atlas?"))[0].id, "projects/atlas");
|
|
assert.deepEqual(await store.hints("What did we discuss about this project?"), []);
|
|
assert.deepEqual(await store.hints("zephyrquartz"), []);
|
|
|
|
await assert.rejects(() => store.forget({ id: "projects/atlas", confirm: "atlas" }), /confirm must exactly match/);
|
|
assert.equal((await store.forget({ id: "projects/atlas", confirm: "projects/atlas" })).action, "deleted");
|
|
await assert.rejects(() => store.read(["projects/atlas"]), /not found/);
|
|
const log = await readFile(join(root, "log.md"), "utf8");
|
|
assert.doesNotMatch(log, /projects\/atlas|Atlas/);
|
|
assert.match(log, /\*\*Deletion\*\*: Removed a concept/);
|
|
assert.doesNotMatch(await readFile(join(root, "projects", "index.md"), "utf8"), /Atlas/);
|
|
} finally {
|
|
await cleanup(root);
|
|
}
|
|
});
|
|
|
|
test("rejects unsafe or reserved concept ids", async () => {
|
|
const { root, store } = await fixture();
|
|
try {
|
|
for (const id of ["../escape", "/absolute", "People/Alice", "index", "notes/log", "a//b"]) {
|
|
assert.throws(() => store.normalizeId(id));
|
|
}
|
|
} finally {
|
|
await cleanup(root);
|
|
}
|
|
});
|