feat: integrate Cortex local memory into OpenClaw
This commit is contained in:
parent
ad2bfb498f
commit
5271cf5c05
@ -5,8 +5,16 @@ import { Command } from "commander";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getMemorySearchManager = vi.fn();
|
||||
const getCortexStatus = vi.fn();
|
||||
const previewCortexContext = vi.fn();
|
||||
const getCortexModeOverride = vi.fn();
|
||||
const setCortexModeOverride = vi.fn();
|
||||
const clearCortexModeOverride = vi.fn();
|
||||
const loadConfig = vi.fn(() => ({}));
|
||||
const readConfigFileSnapshot = vi.fn();
|
||||
const writeConfigFile = vi.fn(async () => {});
|
||||
const resolveDefaultAgentId = vi.fn(() => "main");
|
||||
const resolveAgentWorkspaceDir = vi.fn(() => "/tmp/openclaw-workspace");
|
||||
const resolveCommandSecretRefsViaGateway = vi.fn(async ({ config }: { config: unknown }) => ({
|
||||
resolvedConfig: config,
|
||||
diagnostics: [] as string[],
|
||||
@ -16,12 +24,26 @@ vi.mock("../memory/index.js", () => ({
|
||||
getMemorySearchManager,
|
||||
}));
|
||||
|
||||
vi.mock("../memory/cortex.js", () => ({
|
||||
getCortexStatus,
|
||||
previewCortexContext,
|
||||
}));
|
||||
|
||||
vi.mock("../memory/cortex-mode-overrides.js", () => ({
|
||||
getCortexModeOverride,
|
||||
setCortexModeOverride,
|
||||
clearCortexModeOverride,
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig,
|
||||
readConfigFileSnapshot,
|
||||
writeConfigFile,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/agent-scope.js", () => ({
|
||||
resolveDefaultAgentId,
|
||||
resolveAgentWorkspaceDir,
|
||||
}));
|
||||
|
||||
vi.mock("./command-secret-gateway.js", () => ({
|
||||
@ -42,6 +64,13 @@ beforeAll(async () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
getMemorySearchManager.mockClear();
|
||||
getCortexStatus.mockClear();
|
||||
previewCortexContext.mockClear();
|
||||
getCortexModeOverride.mockClear();
|
||||
setCortexModeOverride.mockClear();
|
||||
clearCortexModeOverride.mockClear();
|
||||
readConfigFileSnapshot.mockClear();
|
||||
writeConfigFile.mockClear();
|
||||
resolveCommandSecretRefsViaGateway.mockClear();
|
||||
process.exitCode = undefined;
|
||||
setVerbose(false);
|
||||
@ -87,6 +116,21 @@ describe("memory cli", () => {
|
||||
getMemorySearchManager.mockResolvedValueOnce({ manager });
|
||||
}
|
||||
|
||||
function mockWritableConfigSnapshot(resolved: Record<string, unknown>) {
|
||||
readConfigFileSnapshot.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
config: resolved,
|
||||
resolved,
|
||||
issues: [],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
path: "/tmp/openclaw.json",
|
||||
raw: JSON.stringify(resolved),
|
||||
parsed: resolved,
|
||||
});
|
||||
}
|
||||
|
||||
function setupMemoryStatusWithInactiveSecretDiagnostics(close: ReturnType<typeof vi.fn>) {
|
||||
resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({
|
||||
resolvedConfig: {},
|
||||
@ -252,6 +296,11 @@ describe("memory cli", () => {
|
||||
expect(helpText).toContain("Quick search using positional query.");
|
||||
expect(helpText).toContain('openclaw memory search --query "deployment" --max-results 20');
|
||||
expect(helpText).toContain("Limit results for focused troubleshooting.");
|
||||
expect(helpText).toContain("openclaw memory cortex status");
|
||||
expect(helpText).toContain("Check local Cortex bridge availability.");
|
||||
expect(helpText).toContain("openclaw memory cortex preview --mode technical");
|
||||
expect(helpText).toContain("openclaw memory cortex enable --mode technical");
|
||||
expect(helpText).toContain("openclaw memory cortex mode set minimal --session-id abc123");
|
||||
});
|
||||
|
||||
it("prints vector error when unavailable", async () => {
|
||||
@ -565,4 +614,188 @@ describe("memory cli", () => {
|
||||
expect(payload.results as unknown[]).toHaveLength(1);
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prints Cortex bridge status", async () => {
|
||||
getCortexStatus.mockResolvedValueOnce({
|
||||
available: true,
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
graphPath: "/tmp/openclaw-workspace/.cortex/context.json",
|
||||
graphExists: true,
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs();
|
||||
await runMemoryCli(["cortex", "status"]);
|
||||
|
||||
expect(getCortexStatus).toHaveBeenCalledWith({
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
graphPath: undefined,
|
||||
});
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Cortex Bridge"));
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("CLI: ready"));
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Graph: present"));
|
||||
});
|
||||
|
||||
it("prints Cortex bridge status as json", async () => {
|
||||
getCortexStatus.mockResolvedValueOnce({
|
||||
available: false,
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
graphPath: "/tmp/openclaw-workspace/.cortex/context.json",
|
||||
graphExists: false,
|
||||
error: "spawn cortex ENOENT",
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs();
|
||||
await runMemoryCli(["cortex", "status", "--json"]);
|
||||
|
||||
const payload = firstLoggedJson(log);
|
||||
expect(payload.agentId).toBe("main");
|
||||
expect(payload.available).toBe(false);
|
||||
expect(payload.error).toBe("spawn cortex ENOENT");
|
||||
});
|
||||
|
||||
it("prints Cortex preview context", async () => {
|
||||
previewCortexContext.mockResolvedValueOnce({
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
graphPath: "/tmp/openclaw-workspace/.cortex/context.json",
|
||||
policy: "technical",
|
||||
maxChars: 1500,
|
||||
context: "## Cortex Context\n- Python",
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs();
|
||||
await runMemoryCli(["cortex", "preview", "--mode", "technical"]);
|
||||
|
||||
expect(previewCortexContext).toHaveBeenCalledWith({
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
graphPath: undefined,
|
||||
policy: "technical",
|
||||
maxChars: undefined,
|
||||
});
|
||||
expect(log).toHaveBeenCalledWith("## Cortex Context\n- Python");
|
||||
});
|
||||
|
||||
it("fails Cortex preview when bridge errors", async () => {
|
||||
previewCortexContext.mockRejectedValueOnce(new Error("Cortex graph not found"));
|
||||
|
||||
const error = spyRuntimeErrors();
|
||||
await runMemoryCli(["cortex", "preview"]);
|
||||
|
||||
expect(error).toHaveBeenCalledWith("Cortex graph not found");
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("enables Cortex prompt bridge in agent defaults", async () => {
|
||||
mockWritableConfigSnapshot({});
|
||||
|
||||
const log = spyRuntimeLogs();
|
||||
await runMemoryCli(["cortex", "enable", "--mode", "professional", "--max-chars", "2200"]);
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledWith({
|
||||
agents: {
|
||||
defaults: {
|
||||
cortex: {
|
||||
enabled: true,
|
||||
mode: "professional",
|
||||
maxChars: 2200,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
"Enabled Cortex prompt bridge for agent defaults (professional, 2200 chars).",
|
||||
);
|
||||
});
|
||||
|
||||
it("disables Cortex prompt bridge for a specific agent", async () => {
|
||||
mockWritableConfigSnapshot({
|
||||
agents: {
|
||||
list: [{ id: "oracle", cortex: { enabled: true, mode: "technical", maxChars: 1500 } }],
|
||||
},
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs();
|
||||
await runMemoryCli(["cortex", "disable", "--agent", "oracle"]);
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledWith({
|
||||
agents: {
|
||||
list: [{ id: "oracle", cortex: { enabled: false, mode: "technical", maxChars: 1500 } }],
|
||||
},
|
||||
});
|
||||
expect(log).toHaveBeenCalledWith("Disabled Cortex prompt bridge for agent oracle.");
|
||||
});
|
||||
|
||||
it("fails Cortex enable for an unknown agent", async () => {
|
||||
mockWritableConfigSnapshot({
|
||||
agents: {
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
});
|
||||
|
||||
const error = spyRuntimeErrors();
|
||||
await runMemoryCli(["cortex", "enable", "--agent", "oracle"]);
|
||||
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(error).toHaveBeenCalledWith("Agent not found: oracle");
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("sets a session-level Cortex mode override", async () => {
|
||||
setCortexModeOverride.mockResolvedValueOnce({
|
||||
agentId: "main",
|
||||
scope: "session",
|
||||
targetId: "session-1",
|
||||
mode: "minimal",
|
||||
updatedAt: "2026-03-08T23:00:00.000Z",
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs();
|
||||
await runMemoryCli(["cortex", "mode", "set", "minimal", "--session-id", "session-1"]);
|
||||
|
||||
expect(setCortexModeOverride).toHaveBeenCalledWith({
|
||||
agentId: "main",
|
||||
scope: "session",
|
||||
targetId: "session-1",
|
||||
mode: "minimal",
|
||||
});
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
"Set Cortex mode override for session session-1 to minimal (main).",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a stored channel-level Cortex mode override as json", async () => {
|
||||
getCortexModeOverride.mockResolvedValueOnce({
|
||||
agentId: "main",
|
||||
scope: "channel",
|
||||
targetId: "slack",
|
||||
mode: "professional",
|
||||
updatedAt: "2026-03-08T23:00:00.000Z",
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs();
|
||||
await runMemoryCli(["cortex", "mode", "show", "--channel", "slack", "--json"]);
|
||||
|
||||
const payload = firstLoggedJson(log);
|
||||
expect(payload.agentId).toBe("main");
|
||||
expect(payload.scope).toBe("channel");
|
||||
expect(payload.targetId).toBe("slack");
|
||||
expect(payload.override).toMatchObject({ mode: "professional" });
|
||||
});
|
||||
|
||||
it("rejects ambiguous Cortex mode targets", async () => {
|
||||
const error = spyRuntimeErrors();
|
||||
await runMemoryCli([
|
||||
"cortex",
|
||||
"mode",
|
||||
"set",
|
||||
"technical",
|
||||
"--session-id",
|
||||
"session-1",
|
||||
"--channel",
|
||||
"slack",
|
||||
]);
|
||||
|
||||
expect(setCortexModeOverride).not.toHaveBeenCalled();
|
||||
expect(error).toHaveBeenCalledWith("Choose either --session-id or --channel, not both.");
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user