Files
kimiko-agent/extensions/memory/test/store.test.mjs
syntaxbullet 91e5f44cc8 feat(memory): add memory extension with core functionality and tests
- 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.
2026-07-23 20:35:08 +02:00

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);
}
});