refactor: update memory skill loader, plugin config, and add privacy docs (#4905)

This commit is contained in:
Kartik
2026-04-22 17:15:19 +05:30
committed by GitHub
parent 32b74e18b7
commit f5dc825d47
21 changed files with 1734 additions and 189 deletions
+296
View File
@@ -32,6 +32,7 @@ vi.mock("../skill-loader.ts", () => ({
loadDreamPrompt: vi.fn().mockReturnValue("dream prompt"),
}));
// ---------------------------------------------------------------------------
// Imports (after mocks)
// ---------------------------------------------------------------------------
@@ -251,6 +252,7 @@ describe("registerCliCommands", () => {
warn: ReturnType<typeof vi.spyOn>;
};
let stderrSpy: ReturnType<typeof vi.spyOn>;
let stdoutSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.resetAllMocks();
@@ -267,6 +269,7 @@ describe("registerCliCommands", () => {
warn: vi.spyOn(console, "warn").mockImplementation(() => {}),
};
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
});
afterEach(() => {
@@ -274,6 +277,7 @@ describe("registerCliCommands", () => {
consoleSpy.error.mockRestore();
consoleSpy.warn.mockRestore();
stderrSpy.mockRestore();
stdoutSpy.mockRestore();
vi.restoreAllMocks();
});
@@ -527,6 +531,158 @@ describe("registerCliCommands", () => {
vi.unstubAllGlobals();
});
it("outputs JSON for --api-key flow when --json is set", async () => {
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({}),
}));
await initCmd._action!({ apiKey: "m0-key", json: true });
const jsonCall = stdoutSpy.mock.calls.find((c) => {
try {
const p = JSON.parse(c[0] as string);
return typeof p.ok === "boolean";
} catch { return false; }
});
expect(jsonCall).toBeDefined();
const parsed = JSON.parse(jsonCall![0] as string);
expect(parsed.ok).toBe(true);
expect(parsed.mode).toBe("platform");
expect(parsed.validated).toBe(true);
vi.unstubAllGlobals();
});
it("outputs JSON for --api-key flow with failed validation when --json is set", async () => {
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: false,
status: 401,
json: vi.fn().mockResolvedValue({}),
}));
await initCmd._action!({ apiKey: "bad-key", json: true });
const jsonCall = stdoutSpy.mock.calls.find((c) => {
try {
const p = JSON.parse(c[0] as string);
return typeof p.ok === "boolean";
} catch { return false; }
});
expect(jsonCall).toBeDefined();
const parsed = JSON.parse(jsonCall![0] as string);
expect(parsed.ok).toBe(false);
expect(parsed.mode).toBe("platform");
expect(parsed.validated).toBe(false);
expect(parsed.httpStatus).toBe(401);
vi.unstubAllGlobals();
});
it("outputs JSON for --api-key + --email conflict when --json is set", async () => {
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
await initCmd._action!({ apiKey: "key", email: "a@b.com", json: true });
const jsonCall = stdoutSpy.mock.calls.find((c) => {
try {
const p = JSON.parse(c[0] as string);
return p.ok === false;
} catch { return false; }
});
expect(jsonCall).toBeDefined();
const parsed = JSON.parse(jsonCall![0] as string);
expect(parsed.error).toContain("Cannot use both");
expect(writePluginAuth).not.toHaveBeenCalled();
});
it("outputs JSON for email send-code flow when --json is set", async () => {
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({}),
}));
await initCmd._action!({ email: "user@example.com", json: true });
const jsonCall = stdoutSpy.mock.calls.find((c) => {
try {
const p = JSON.parse(c[0] as string);
return p.codeSent === true;
} catch { return false; }
});
expect(jsonCall).toBeDefined();
const parsed = JSON.parse(jsonCall![0] as string);
expect(parsed.ok).toBe(true);
expect(parsed.email).toBe("user@example.com");
expect(parsed.nextCommand).toContain("--code");
vi.unstubAllGlobals();
});
it("outputs JSON for email verify flow when --json is set", async () => {
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({ api_key: "m0-verified" }),
}));
await initCmd._action!({ email: "u@b.com", code: "123456", json: true });
const jsonCall = stdoutSpy.mock.calls.find((c) => {
try {
const p = JSON.parse(c[0] as string);
return p.ok === true && p.mode === "platform";
} catch { return false; }
});
expect(jsonCall).toBeDefined();
const parsed = JSON.parse(jsonCall![0] as string);
expect(parsed.email).toBe("u@b.com");
expect(parsed.message).toContain("Authenticated");
vi.unstubAllGlobals();
});
it("clears stale apiKey when switching to OSS mode", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }));
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValue({
apiKey: "m0-old-platform-key",
mode: "platform",
userId: "testuser",
});
await initCmd._action!({
mode: "open-source",
ossLlm: "ollama",
ossEmbedder: "ollama",
ossVector: "qdrant",
});
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: "",
mode: "open-source",
}),
);
vi.unstubAllGlobals();
});
});
// ========================================================================
@@ -1456,4 +1612,144 @@ describe("registerCliCommands", () => {
);
});
});
// ========================================================================
// Restructured init menu flags
// ========================================================================
describe("init — restructured menu", () => {
it("registers --mode flag", () => {
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
const modeOpt = initCmd._options.find((o) => o.flags.includes("--mode"));
expect(modeOpt).toBeDefined();
});
it("registers --oss-llm flag", () => {
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
const opt = initCmd._options.find((o) => o.flags.includes("--oss-llm "));
expect(opt).toBeDefined();
});
it("registers --json flag on init", () => {
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
const opt = initCmd._options.find((o) => o.flags.includes("--json"));
expect(opt).toBeDefined();
});
});
// ========================================================================
// --json flag registration on all commands
// ========================================================================
describe("--json flag registration", () => {
for (const name of ["search", "add", "get", "list", "update", "delete", "status", "import", "dream"]) {
it(`registers --json on ${name}`, () => {
const { mem0 } = setup();
const cmd = findCommand(mem0, name)!;
const opt = cmd._options.find((o) => o.flags.includes("--json"));
expect(opt).toBeDefined();
});
}
it("registers --json on config show", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
const showCmd = findCommand(configCmd, "show")!;
expect(showCmd).toBeDefined();
const opt = showCmd._options.find((o) => o.flags.includes("--json"));
expect(opt).toBeDefined();
});
});
// ========================================================================
// Non-interactive OSS init
// ========================================================================
describe("init --mode open-source (non-interactive)", () => {
it("writes LLM, embedder, and vector config for ollama + qdrant", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }));
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
await initCmd._action!({
mode: "open-source",
ossLlm: "ollama",
ossEmbedder: "ollama",
ossVector: "qdrant",
userId: "test-user",
});
expect(writePluginConfigField).toHaveBeenCalledWith(
["oss", "llm"],
expect.objectContaining({ provider: "ollama" }),
);
expect(writePluginConfigField).toHaveBeenCalledWith(
["oss", "embedder"],
expect.objectContaining({ provider: "ollama" }),
);
expect(writePluginConfigField).toHaveBeenCalledWith(
["oss", "vectorStore"],
expect.objectContaining({ provider: "qdrant" }),
);
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({ mode: "open-source", userId: "test-user" }),
);
vi.unstubAllGlobals();
});
it("outputs JSON when --json is passed", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }));
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
await initCmd._action!({
mode: "open-source",
ossLlm: "ollama",
ossEmbedder: "ollama",
ossVector: "qdrant",
json: true,
});
const jsonCall = stdoutSpy.mock.calls.find((c) => {
try {
const p = JSON.parse(c[0] as string);
return p.ok === true;
} catch {
return false;
}
});
expect(jsonCall).toBeDefined();
if (jsonCall) {
const parsed = JSON.parse(jsonCall[0] as string);
expect(parsed.mode).toBe("open-source");
expect(parsed.config.llm.provider).toBe("ollama");
}
vi.unstubAllGlobals();
});
it("errors when openai LLM has no key", async () => {
const { mem0 } = setup();
const initCmd = findCommand(mem0, "init")!;
// Ensure env var is not set so validation fails
const savedEnv = process.env.OPENAI_API_KEY;
delete process.env.OPENAI_API_KEY;
await initCmd._action!({ mode: "open-source", ossLlm: "openai" });
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("--oss-llm-key"),
);
// Restore env var
if (savedEnv !== undefined) process.env.OPENAI_API_KEY = savedEnv;
});
});
});
+4 -4
View File
@@ -44,14 +44,14 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
expect(cfg.userId.length).toBeGreaterThan(0);
});
it("autoCapture defaults to true", () => {
it("autoCapture defaults to false", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.autoCapture).toBe(true);
expect(cfg.autoCapture).toBe(false);
});
it("autoRecall defaults to true", () => {
it("autoRecall defaults to false", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.autoRecall).toBe(true);
expect(cfg.autoRecall).toBe(false);
});
it("topK defaults to 5", () => {
+55
View File
@@ -0,0 +1,55 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { jsonOut, jsonErr, redactSecrets } from "../cli/json-helpers.ts";
describe("jsonOut", () => {
let writeSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); });
afterEach(() => { writeSpy.mockRestore(); });
it("returns false and prints nothing when json is falsy", () => {
expect(jsonOut({}, { ok: true })).toBe(false);
expect(writeSpy).not.toHaveBeenCalled();
});
it("returns true and prints JSON to stdout when json is true", () => {
expect(jsonOut({ json: true }, { ok: true, count: 3 })).toBe(true);
expect(writeSpy).toHaveBeenCalledOnce();
const parsed = JSON.parse(writeSpy.mock.calls[0][0] as string);
expect(parsed).toEqual({ ok: true, count: 3 });
});
});
describe("jsonErr", () => {
let writeSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); });
afterEach(() => { writeSpy.mockRestore(); });
it("returns false when json is falsy", () => {
expect(jsonErr({}, "bad")).toBe(false);
});
it("returns true and prints error JSON to stdout", () => {
expect(jsonErr({ json: true }, "Something broke")).toBe(true);
const parsed = JSON.parse(writeSpy.mock.calls[0][0] as string);
expect(parsed).toEqual({ ok: false, error: "Something broke" });
});
});
describe("redactSecrets", () => {
it("redacts string values for known secret keys", () => {
const input = { apiKey: "m0-abcdefghijklmnop", name: "test" };
const result = redactSecrets(input, new Set(["apiKey"]));
expect(result.apiKey).toBe("m0-a...mnop");
expect(result.name).toBe("test");
});
it("handles short keys", () => {
const result = redactSecrets({ apiKey: "ab" }, new Set(["apiKey"]));
expect(result.apiKey).toBe("ab***");
});
it("skips non-string values", () => {
const result = redactSecrets({ count: 5 }, new Set(["count"]));
expect(result.count).toBe(5);
});
});
+196
View File
@@ -0,0 +1,196 @@
import { describe, it, expect } from "vitest";
import {
LLM_PROVIDERS,
EMBEDDER_PROVIDERS,
VECTOR_PROVIDERS,
KNOWN_EMBEDDER_DIMS,
buildOssLlmConfig,
buildOssEmbedderConfig,
buildOssVectorConfig,
validateOssFlags,
checkQdrantConnectivity,
checkOllamaConnectivity,
checkPgConnectivity,
} from "../cli/oss-wizard.ts";
describe("LLM_PROVIDERS", () => {
it("has 3 providers", () => {
expect(LLM_PROVIDERS).toHaveLength(3);
expect(LLM_PROVIDERS.map((p) => p.id)).toEqual(["openai", "ollama", "anthropic"]);
});
it("openai requires API key", () => {
const openai = LLM_PROVIDERS.find((p) => p.id === "openai")!;
expect(openai.needsApiKey).toBe(true);
expect(openai.defaultModel).toBe("gpt-5-mini");
});
it("ollama needs no API key but needs URL", () => {
const ollama = LLM_PROVIDERS.find((p) => p.id === "ollama")!;
expect(ollama.needsApiKey).toBe(false);
expect(ollama.needsUrl).toBe(true);
expect(ollama.defaultUrl).toBe("http://localhost:11434");
});
});
describe("EMBEDDER_PROVIDERS", () => {
it("has 2 providers", () => {
expect(EMBEDDER_PROVIDERS).toHaveLength(2);
});
});
describe("KNOWN_EMBEDDER_DIMS", () => {
it("maps default models to dims", () => {
expect(KNOWN_EMBEDDER_DIMS["text-embedding-3-small"]).toBe(1536);
expect(KNOWN_EMBEDDER_DIMS["nomic-embed-text"]).toBe(768);
});
});
describe("buildOssLlmConfig", () => {
it("builds openai config with API key", () => {
const result = buildOssLlmConfig("openai", { apiKey: "sk-test" });
expect(result).toEqual({
provider: "openai",
config: { model: "gpt-5-mini", apiKey: "sk-test" },
});
});
it("builds ollama config with custom URL and model", () => {
const result = buildOssLlmConfig("ollama", { url: "http://myhost:11434", model: "mistral" });
expect(result).toEqual({
provider: "ollama",
config: { model: "mistral", url: "http://myhost:11434" },
});
});
it("builds ollama config with default URL", () => {
const result = buildOssLlmConfig("ollama", {});
expect(result.config.url).toBe("http://localhost:11434");
});
it("ignores url for non-ollama providers", () => {
const result = buildOssLlmConfig("anthropic", { apiKey: "sk-ant", url: "http://ignored" });
expect(result.config).not.toHaveProperty("url");
expect(result.config).toHaveProperty("apiKey", "sk-ant");
});
});
describe("buildOssEmbedderConfig", () => {
it("builds openai embedder", () => {
const result = buildOssEmbedderConfig("openai", { apiKey: "sk-test" });
expect(result.config.model).toBe("text-embedding-3-small");
expect(result.dims).toBe(1536);
});
it("builds ollama embedder with url field", () => {
const result = buildOssEmbedderConfig("ollama", { url: "http://myhost:11434" });
expect(result.config.url).toBe("http://myhost:11434");
expect(result.config).not.toHaveProperty("ollama_base_url");
expect(result.config.model).toBe("nomic-embed-text");
expect(result.dims).toBe(768);
});
it("returns unknown dims for custom model", () => {
const result = buildOssEmbedderConfig("ollama", { model: "custom-embed" });
expect(result.dims).toBeUndefined();
});
});
describe("VECTOR_PROVIDERS", () => {
it("has 2 providers", () => {
expect(VECTOR_PROVIDERS).toHaveLength(2);
expect(VECTOR_PROVIDERS.map((p) => p.id)).toEqual(["qdrant", "pgvector"]);
});
it("qdrant requires server connection", () => {
const qdrant = VECTOR_PROVIDERS.find((p) => p.id === "qdrant")!;
expect(qdrant.needsConnection).toBe(true);
expect(qdrant.defaultUrl).toBe("http://localhost:6333");
expect(qdrant.setupHint).toContain("docker");
});
it("pgvector requires connection and has setup hint", () => {
const pg = VECTOR_PROVIDERS.find((p) => p.id === "pgvector")!;
expect(pg.needsConnection).toBe(true);
expect(pg.defaultPort).toBe(5432);
expect(pg.setupHint).toContain("pgvector");
});
});
describe("buildOssVectorConfig", () => {
it("builds qdrant with default url and dims", () => {
const result = buildOssVectorConfig("qdrant", { dims: 1536 });
expect(result.config.url).toBe("http://localhost:6333");
expect(result.config.onDisk).toBe(true);
expect(result.config.dimension).toBe(1536);
});
it("builds qdrant with custom url", () => {
const result = buildOssVectorConfig("qdrant", { url: "http://qdrant.local:6333", dims: 768 });
expect(result.config.url).toBe("http://qdrant.local:6333");
expect(result.config.onDisk).toBe(true);
expect(result.config.dimension).toBe(768);
});
it("builds qdrant with api key for cloud", () => {
const result = buildOssVectorConfig("qdrant", { url: "https://cloud.qdrant.io", apiKey: "qd-key", dims: 1536 });
expect(result.config.apiKey).toBe("qd-key");
expect(result.config.url).toBe("https://cloud.qdrant.io");
});
it("builds pgvector with connection details", () => {
const result = buildOssVectorConfig("pgvector", {
host: "db.local", port: "5432", user: "me", password: "pw", dbname: "mydb", dims: 512,
});
expect(result.config.host).toBe("db.local");
expect(result.config.dimension).toBe(512);
});
});
describe("checkQdrantConnectivity", () => {
it("returns error for unreachable host", async () => {
const result = await checkQdrantConnectivity("http://localhost:19999");
expect(result.ok).toBe(false);
expect(result.error).toContain("Cannot reach Qdrant");
});
});
describe("checkOllamaConnectivity", () => {
it("returns error for unreachable host", async () => {
const result = await checkOllamaConnectivity("http://localhost:19998");
expect(result.ok).toBe(false);
expect(result.error).toContain("Cannot reach Ollama");
});
});
describe("checkPgConnectivity", () => {
it("returns error for unreachable host", async () => {
const result = await checkPgConnectivity("localhost", 19997);
expect(result.ok).toBe(false);
expect(result.error).toContain("PostgreSQL not reachable");
});
});
describe("validateOssFlags", () => {
it("returns error when openai LLM has no key", () => {
const result = validateOssFlags({ ossLlm: "openai" });
expect(result.error).toContain("--oss-llm-key");
});
it("passes for ollama with no key", () => {
const result = validateOssFlags({ ossLlm: "ollama", ossEmbedder: "ollama", ossVector: "qdrant" });
expect(result.error).toBeUndefined();
});
it("returns error for unknown provider", () => {
const result = validateOssFlags({ ossLlm: "bogus" });
expect(result.error).toContain("Unknown LLM provider");
});
it("returns error when pgvector missing user", () => {
const result = validateOssFlags({
ossLlm: "ollama", ossEmbedder: "ollama", ossVector: "pgvector",
});
expect(result.error).toContain("--oss-vector-user");
});
});
+4 -3
View File
@@ -263,10 +263,11 @@ describe("memory_search execute", () => {
scope: "session",
});
// Should call buildSearchOptions with session ID
// Should call buildSearchOptions with session ID as 4th arg (sessionKey)
expect(ctx.buildSearchOptions).toHaveBeenCalledWith(
"testuser",
undefined,
undefined,
"session-abc",
);
expect(result.details.count).toBe(1);
@@ -446,8 +447,8 @@ describe("memory_add execute", () => {
await tool.execute("call-7", { text: "new fact" });
// Search should be called for dedup before add
expect(searchMock).toHaveBeenCalledOnce();
// Mem0 backend handles dedup internally — no separate search call
expect(searchMock).not.toHaveBeenCalled();
expect(addMock).toHaveBeenCalledOnce();
});
});