feat: integrate Cortex local memory into OpenClaw
This commit is contained in:
parent
3f97d71891
commit
b3be74d1dd
220
src/memory/cortex.test.ts
Normal file
220
src/memory/cortex.test.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { runExec } = vi.hoisted(() => ({
|
||||
runExec: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runExec,
|
||||
}));
|
||||
|
||||
import {
|
||||
getCortexStatus,
|
||||
ingestCortexMemoryFromText,
|
||||
listCortexMemoryConflicts,
|
||||
previewCortexContext,
|
||||
resolveCortexGraphPath,
|
||||
resolveCortexMemoryConflict,
|
||||
syncCortexCodingContext,
|
||||
} from "./cortex.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
runExec.mockReset();
|
||||
});
|
||||
|
||||
describe("cortex bridge", () => {
|
||||
it("resolves the default graph path inside the workspace", () => {
|
||||
expect(resolveCortexGraphPath("/tmp/workspace")).toBe("/tmp/workspace/.cortex/context.json");
|
||||
});
|
||||
|
||||
it("resolves relative graph overrides against the workspace", () => {
|
||||
expect(resolveCortexGraphPath("/tmp/workspace", "graphs/main.json")).toBe(
|
||||
path.normalize("/tmp/workspace/graphs/main.json"),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports availability and graph presence", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cortex-status-"));
|
||||
const graphPath = path.join(tmpDir, ".cortex", "context.json");
|
||||
await fs.mkdir(path.dirname(graphPath), { recursive: true });
|
||||
await fs.writeFile(graphPath, "{}", "utf8");
|
||||
runExec.mockResolvedValueOnce({ stdout: "", stderr: "" });
|
||||
|
||||
const status = await getCortexStatus({ workspaceDir: tmpDir });
|
||||
|
||||
expect(status.available).toBe(true);
|
||||
expect(status.graphExists).toBe(true);
|
||||
expect(status.graphPath).toBe(graphPath);
|
||||
});
|
||||
|
||||
it("surfaces Cortex CLI errors in status", async () => {
|
||||
runExec.mockRejectedValueOnce(new Error("spawn cortex ENOENT"));
|
||||
|
||||
const status = await getCortexStatus({ workspaceDir: "/tmp/workspace" });
|
||||
|
||||
expect(status.available).toBe(false);
|
||||
expect(status.error).toContain("spawn cortex ENOENT");
|
||||
});
|
||||
|
||||
it("exports preview context", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cortex-preview-"));
|
||||
const graphPath = path.join(tmpDir, ".cortex", "context.json");
|
||||
await fs.mkdir(path.dirname(graphPath), { recursive: true });
|
||||
await fs.writeFile(graphPath, "{}", "utf8");
|
||||
runExec
|
||||
.mockResolvedValueOnce({ stdout: "", stderr: "" })
|
||||
.mockResolvedValueOnce({ stdout: "## Cortex Context\n- Python\n", stderr: "" });
|
||||
|
||||
const preview = await previewCortexContext({
|
||||
workspaceDir: tmpDir,
|
||||
policy: "technical",
|
||||
maxChars: 500,
|
||||
});
|
||||
|
||||
expect(preview.graphPath).toBe(graphPath);
|
||||
expect(preview.policy).toBe("technical");
|
||||
expect(preview.maxChars).toBe(500);
|
||||
expect(preview.context).toBe("## Cortex Context\n- Python");
|
||||
});
|
||||
|
||||
it("fails preview when graph is missing", async () => {
|
||||
runExec.mockResolvedValueOnce({ stdout: "", stderr: "" });
|
||||
|
||||
await expect(previewCortexContext({ workspaceDir: "/tmp/workspace" })).rejects.toThrow(
|
||||
"Cortex graph not found",
|
||||
);
|
||||
});
|
||||
|
||||
it("lists memory conflicts from Cortex JSON output", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cortex-conflicts-"));
|
||||
const graphPath = path.join(tmpDir, ".cortex", "context.json");
|
||||
await fs.mkdir(path.dirname(graphPath), { recursive: true });
|
||||
await fs.writeFile(graphPath, "{}", "utf8");
|
||||
runExec.mockResolvedValueOnce({ stdout: "", stderr: "" }).mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
conflicts: [
|
||||
{
|
||||
id: "conf_1",
|
||||
type: "temporal_flip",
|
||||
severity: 0.91,
|
||||
summary: "Hiring status changed",
|
||||
},
|
||||
],
|
||||
}),
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const conflicts = await listCortexMemoryConflicts({ workspaceDir: tmpDir });
|
||||
|
||||
expect(conflicts).toEqual([
|
||||
{
|
||||
id: "conf_1",
|
||||
type: "temporal_flip",
|
||||
severity: 0.91,
|
||||
summary: "Hiring status changed",
|
||||
nodeLabel: undefined,
|
||||
oldValue: undefined,
|
||||
newValue: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves memory conflicts from Cortex JSON output", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cortex-resolve-"));
|
||||
const graphPath = path.join(tmpDir, ".cortex", "context.json");
|
||||
await fs.mkdir(path.dirname(graphPath), { recursive: true });
|
||||
await fs.writeFile(graphPath, "{}", "utf8");
|
||||
runExec.mockResolvedValueOnce({ stdout: "", stderr: "" }).mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
status: "ok",
|
||||
conflict_id: "conf_1",
|
||||
nodes_updated: 1,
|
||||
nodes_removed: 1,
|
||||
commit_id: "ver_123",
|
||||
}),
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const result = await resolveCortexMemoryConflict({
|
||||
workspaceDir: tmpDir,
|
||||
conflictId: "conf_1",
|
||||
action: "accept-new",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "ok",
|
||||
conflictId: "conf_1",
|
||||
action: "accept-new",
|
||||
nodesUpdated: 1,
|
||||
nodesRemoved: 1,
|
||||
commitId: "ver_123",
|
||||
message: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("syncs coding context to default coding platforms", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cortex-sync-"));
|
||||
const graphPath = path.join(tmpDir, ".cortex", "context.json");
|
||||
await fs.mkdir(path.dirname(graphPath), { recursive: true });
|
||||
await fs.writeFile(graphPath, "{}", "utf8");
|
||||
runExec
|
||||
.mockResolvedValueOnce({ stdout: "", stderr: "" })
|
||||
.mockResolvedValueOnce({ stdout: "", stderr: "" });
|
||||
|
||||
const result = await syncCortexCodingContext({
|
||||
workspaceDir: tmpDir,
|
||||
policy: "technical",
|
||||
});
|
||||
|
||||
expect(result.policy).toBe("technical");
|
||||
expect(result.platforms).toEqual(["claude-code", "cursor", "copilot"]);
|
||||
expect(runExec).toHaveBeenLastCalledWith(
|
||||
"cortex",
|
||||
[
|
||||
"context-write",
|
||||
graphPath,
|
||||
"--platforms",
|
||||
"claude-code",
|
||||
"cursor",
|
||||
"copilot",
|
||||
"--policy",
|
||||
"technical",
|
||||
],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("ingests high-signal text into the Cortex graph with merge", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cortex-ingest-"));
|
||||
const graphPath = path.join(tmpDir, ".cortex", "context.json");
|
||||
runExec
|
||||
.mockResolvedValueOnce({ stdout: "", stderr: "" })
|
||||
.mockResolvedValueOnce({ stdout: "", stderr: "" });
|
||||
|
||||
const result = await ingestCortexMemoryFromText({
|
||||
workspaceDir: tmpDir,
|
||||
event: {
|
||||
actor: "user",
|
||||
text: "I prefer concise answers and I am focused on fundraising this quarter.",
|
||||
sessionId: "session-1",
|
||||
channelId: "channel-1",
|
||||
agentId: "main",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
workspaceDir: tmpDir,
|
||||
graphPath,
|
||||
stored: true,
|
||||
});
|
||||
expect(runExec).toHaveBeenLastCalledWith(
|
||||
"cortex",
|
||||
expect.arrayContaining(["extract", "-o", graphPath, "--merge", graphPath]),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user