openclaw-cortex/test/narrative-generator.test.ts
Claudia d41a13f914 feat: openclaw-cortex v0.1.0 — conversation intelligence plugin
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)
2026-02-17 12:16:49 +01:00

177 lines
5.2 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { NarrativeGenerator, loadDailyNotes, extractTimeline, buildSections, generateStructured } from "../src/narrative-generator.js";
const logger = {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {},
};
function makeWorkspace(): string {
const ws = mkdtempSync(join(tmpdir(), "cortex-narrative-"));
mkdirSync(join(ws, "memory", "reboot"), { recursive: true });
return ws;
}
function writeDailyNote(workspace: string, date: string, content: string) {
writeFileSync(join(workspace, "memory", `${date}.md`), content);
}
function writeThreads(workspace: string, threads: any[]) {
writeFileSync(
join(workspace, "memory", "reboot", "threads.json"),
JSON.stringify({ version: 2, updated: new Date().toISOString(), threads }, null, 2),
);
}
function writeDecisions(workspace: string, decisions: any[]) {
writeFileSync(
join(workspace, "memory", "reboot", "decisions.json"),
JSON.stringify({ version: 1, updated: new Date().toISOString(), decisions }, null, 2),
);
}
describe("NarrativeGenerator", () => {
it("creates instance without errors", () => {
const ws = makeWorkspace();
const gen = new NarrativeGenerator(ws, logger);
expect(gen).toBeTruthy();
});
it("generates empty narrative for empty workspace", () => {
const ws = makeWorkspace();
const gen = new NarrativeGenerator(ws, logger);
const result = gen.generate();
expect(result).toBeTruthy();
expect(typeof result).toBe("string");
});
it("includes date header", () => {
const ws = makeWorkspace();
const gen = new NarrativeGenerator(ws, logger);
const result = gen.generate();
expect(result).toMatch(/\d{4}/); // contains year
});
it("includes open threads", () => {
const ws = makeWorkspace();
writeThreads(ws, [
{
id: "t1",
title: "Auth Migration",
status: "open",
priority: "high",
summary: "Migrating auth system",
decisions: [],
waiting_for: null,
mood: "productive",
last_activity: new Date().toISOString(),
created: new Date().toISOString(),
},
]);
const gen = new NarrativeGenerator(ws, logger);
const result = gen.generate();
expect(result).toContain("Auth Migration");
});
it("includes closed threads as completed", () => {
const ws = makeWorkspace();
const now = new Date();
writeThreads(ws, [
{
id: "t2",
title: "Bug Fix Deploy",
status: "closed",
priority: "medium",
summary: "Fixed critical bug",
decisions: [],
waiting_for: null,
mood: "productive",
last_activity: now.toISOString(),
created: new Date(now.getTime() - 3600000).toISOString(),
},
]);
const gen = new NarrativeGenerator(ws, logger);
const result = gen.generate();
expect(result).toContain("Bug Fix Deploy");
});
it("includes recent decisions", () => {
const ws = makeWorkspace();
writeDecisions(ws, [
{
id: "d1",
what: "Use TypeScript for the plugin",
date: new Date().toISOString().slice(0, 10),
why: "Consistency with OpenClaw",
impact: "high",
who: "albert",
extracted_at: new Date().toISOString(),
},
]);
const gen = new NarrativeGenerator(ws, logger);
const result = gen.generate();
expect(result).toContain("TypeScript");
});
it("includes daily note content when available", () => {
const ws = makeWorkspace();
const today = new Date().toISOString().slice(0, 10);
writeDailyNote(ws, today, "## 10:00\nWorked on plugin architecture\n## 14:00\nCode review");
const gen = new NarrativeGenerator(ws, logger);
const result = gen.generate();
expect(result.length).toBeGreaterThan(0);
});
it("persists narrative to file", () => {
const ws = makeWorkspace();
writeThreads(ws, [
{
id: "t3",
title: "Test Thread",
status: "open",
priority: "medium",
summary: "Testing",
decisions: [],
waiting_for: null,
mood: "neutral",
last_activity: new Date().toISOString(),
created: new Date().toISOString(),
},
]);
const gen = new NarrativeGenerator(ws, logger);
gen.write();
const filePath = join(ws, "memory", "reboot", "narrative.md");
const content = readFileSync(filePath, "utf-8");
expect(content).toContain("Test Thread");
});
it("handles missing threads.json gracefully", () => {
const ws = makeWorkspace();
const gen = new NarrativeGenerator(ws, logger);
expect(() => gen.generate()).not.toThrow();
});
it("handles missing decisions.json gracefully", () => {
const ws = makeWorkspace();
const gen = new NarrativeGenerator(ws, logger);
expect(() => gen.generate()).not.toThrow();
});
it("handles corrupt threads.json", () => {
const ws = makeWorkspace();
writeFileSync(join(ws, "memory", "reboot", "threads.json"), "not json");
const gen = new NarrativeGenerator(ws, logger);
expect(() => gen.generate()).not.toThrow();
});
});