chore: import upstream snapshot with attribution
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:45 +08:00
commit 555e282cc4
1802 changed files with 338080 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
/**
* Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
*
* Mirror of `cli/python/tests/test_agent_mode.py` — both files MUST stay
* in sync so that the Python and Node CLIs expose an identical surface
* for the Agent Mode entrypoint. If you add a flag here, add the same
* assertion on the Python side (and vice versa).
*
* Network-bound bootstrap is covered by the platform-side E2E suite
* (`backend/tests/e2e/test_05_agent_mode.py`); these tests only verify
* the CLI surface that ships in the binary.
*/
import { describe, it, expect } from "vitest";
import { execSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
function run(
args: string[],
opts: { home?: string; env?: Record<string, string> } = {},
): { stdout: string; stderr: string; exitCode: number } {
const env = { ...process.env };
for (const key of Object.keys(env)) {
if (key.startsWith("MEM0_")) delete env[key];
}
if (opts.home) env.HOME = opts.home;
if (opts.env) Object.assign(env, opts.env);
try {
const stdout = execSync(`npx tsx src/index.ts ${args.join(" ")}`, {
cwd: path.join(__dirname, ".."),
env,
encoding: "utf-8",
timeout: 15000,
});
return { stdout, stderr: "", exitCode: 0 };
} catch (e: any) {
return {
stdout: e.stdout ?? "",
stderr: e.stderr ?? "",
exitCode: e.status ?? 1,
};
}
}
function cleanHome(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
}
describe("init flag surface", () => {
it("init --help lists --agent", () => {
const result = run(["init", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--agent");
});
it("init --help describes Agent Mode", () => {
const result = run(["init", "--help"]);
expect(result.exitCode).toBe(0);
// Description must mention what --agent actually does so an agent
// reading the help can self-discover the bootstrap entrypoint.
expect(
result.stdout.includes("Agent Mode") ||
result.stdout.toLowerCase().includes("unattended"),
).toBe(true);
});
it("init --help lists --source", () => {
const result = run(["init", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--source");
});
it("init --help lists --email and --code", () => {
const result = run(["init", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--email");
expect(result.stdout).toContain("--code");
});
});
describe("argv preprocessing — --agent reaches init subcommand", () => {
// Regression for the bug where the global --agent JSON-alias swallowed
// the init-level --agent flag, making `mem0 init --agent` behave like
// the plain interactive wizard.
it("init --agent triggers bootstrap branch (not the wizard)", () => {
const home = cleanHome();
const result = run(["init", "--agent"], {
home,
env: {
MEM0_BASE_URL: "http://127.0.0.1:1", // blackhole
FORCE_COLOR: "0",
},
});
const combined = (result.stdout + result.stderr).toLowerCase();
// Either bootstrap-attempt error, or a connection/network error —
// both prove the --agent path executed (the wizard would prompt for
// input and succeed/hang, not surface a network error).
expect(
combined.includes("agent") ||
combined.includes("connect") ||
combined.includes("network") ||
combined.includes("fetch") ||
combined.includes("bootstrap"),
).toBe(true);
fs.rmSync(home, { recursive: true, force: true });
});
});
describe("JSON envelope on network failure", () => {
it("init --agent --json does not leak a stack trace when backend is unreachable", () => {
const home = cleanHome();
const result = run(["init", "--agent", "--json"], {
home,
env: {
MEM0_BASE_URL: "http://127.0.0.1:1",
FORCE_COLOR: "0",
},
});
const combined = result.stdout + result.stderr;
// No raw Node stack should escape the agent-mode handler.
expect(combined).not.toMatch(/at \w+\s*\(.+\.ts:\d+/);
expect(combined).not.toContain("UnhandledPromiseRejection");
expect(result.exitCode).not.toBe(0);
fs.rmSync(home, { recursive: true, force: true });
});
});
describe("top-level help lists init", () => {
// `mem0 --help` must list `init` so agents walking the top-level help
// can discover the Agent Mode entrypoint without prior knowledge.
it("--help lists init", () => {
const result = run(["--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("init");
});
});
+98
View File
@@ -0,0 +1,98 @@
/**
* Tests for branding utilities.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
BRAND_COLOR,
SUCCESS_COLOR,
ERROR_COLOR,
TAGLINE,
LOGO_MINI,
printSuccess,
printError,
printWarning,
printInfo,
printScope,
} from "../src/branding.js";
let output: string;
let errOutput: string;
const originalLog = console.log;
const originalError = console.error;
beforeEach(() => {
output = "";
errOutput = "";
console.log = (...args: unknown[]) => {
output += args.map(String).join(" ") + "\n";
};
console.error = (...args: unknown[]) => {
errOutput += args.map(String).join(" ") + "\n";
};
});
afterEach(() => {
console.log = originalLog;
console.error = originalError;
});
describe("branding constants", () => {
it("has correct brand color", () => {
expect(BRAND_COLOR).toBe("#8b5cf6");
});
it("has correct tagline", () => {
expect(TAGLINE).toBe("The Memory Layer for AI Agents");
});
it("has correct logo mini", () => {
expect(LOGO_MINI).toBe("◆ mem0");
});
});
describe("printSuccess", () => {
it("prints success message", () => {
printSuccess("Operation completed");
expect(output).toContain("Operation completed");
});
});
describe("printError", () => {
it("prints error message to stderr", () => {
printError("Something failed");
expect(errOutput).toContain("Something failed");
});
it("prints hint when provided to stderr", () => {
printError("Failed", "Try again");
expect(errOutput).toContain("Try again");
});
});
describe("printWarning", () => {
it("prints warning message to stderr", () => {
printWarning("Be careful");
expect(errOutput).toContain("Be careful");
});
});
describe("printInfo", () => {
it("prints info message", () => {
printInfo("Important note");
expect(errOutput).toContain("Important note");
});
});
describe("printScope", () => {
it("prints scope when IDs present", () => {
printScope({ user_id: "alice", agent_id: "bot" });
expect(errOutput).toContain("alice");
expect(errOutput).toContain("bot");
});
it("prints nothing when no IDs", () => {
printScope({});
expect(errOutput).toBe("");
});
});
+149
View File
@@ -0,0 +1,149 @@
/**
* Integration tests — invoke CLI as subprocess to test end-to-end.
*/
import { describe, it, expect } from "vitest";
import { execSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
function run(
args: string[],
opts: { home?: string; env?: Record<string, string> } = {},
): { stdout: string; stderr: string; exitCode: number } {
const env = { ...process.env };
// Strip MEM0_ env vars
for (const key of Object.keys(env)) {
if (key.startsWith("MEM0_")) delete env[key];
}
if (opts.home) env.HOME = opts.home;
if (opts.env) Object.assign(env, opts.env);
try {
const stdout = execSync(
`npx tsx src/index.ts ${args.join(" ")}`,
{ cwd: path.join(__dirname, ".."), env, encoding: "utf-8", timeout: 15000 },
);
return { stdout, stderr: "", exitCode: 0 };
} catch (e: any) {
return {
stdout: e.stdout ?? "",
stderr: e.stderr ?? "",
exitCode: e.status ?? 1,
};
}
}
describe("CLI Integration — help and version", () => {
it("shows help with --help", () => {
const result = run(["--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("mem0");
expect(result.stdout).toContain("add");
expect(result.stdout).toContain("search");
});
it("help --json produces valid JSON", () => {
const result = run(["help", "--json"]);
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.stdout);
// spec may have cli.name or top-level name
const name = parsed.name ?? parsed.cli?.name;
expect(name).toBe("mem0");
});
it("shows add help", () => {
const result = run(["add", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("user-id");
expect(result.stdout).toContain("messages");
});
it("shows search help", () => {
const result = run(["search", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("top-k");
});
it("shows list help", () => {
const result = run(["list", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("page-size");
});
it("shows delete help with --all, --entity, --project", () => {
const result = run(["delete", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--all");
expect(result.stdout).toContain("--entity");
expect(result.stdout).toContain("--project");
expect(result.stdout).toContain("--force");
expect(result.stdout.toLowerCase()).toContain("memory");
});
it("delete with no args errors", () => {
const result = run(["delete"]);
expect(result.exitCode).not.toBe(0);
const combined = result.stdout + result.stderr;
expect(combined).toContain("--all");
});
it("shows entity list help", () => {
const result = run(["entity", "list", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout.toLowerCase()).toContain("entitytype");
});
it("shows entity delete help", () => {
const result = run(["entity", "delete", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--user-id");
expect(result.stdout).toContain("--force");
});
it("shows import help", () => {
const result = run(["import", "--help"]);
expect(result.exitCode).toBe(0);
});
it("add help has --output flag", () => {
const result = run(["add", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--output");
});
it("search help has --rerank flag", () => {
const result = run(["search", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--rerank");
});
it("list help has --category flag", () => {
const result = run(["list", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--category");
});
});
describe("CLI Integration — isolated (clean home)", () => {
function cleanHome(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
}
it("add without API key errors", () => {
const home = cleanHome();
const result = run(["add", "test", "--user-id", "alice"], { home });
expect(result.exitCode).not.toBe(0);
const combined = result.stdout + result.stderr;
expect(combined.toLowerCase()).toMatch(/api.key|error/i);
fs.rmSync(home, { recursive: true, force: true });
});
it("config show works with clean home", () => {
const home = cleanHome();
const result = run(["config", "show"], { home });
expect(result.exitCode).toBe(0);
fs.rmSync(home, { recursive: true, force: true });
});
});
+466
View File
@@ -0,0 +1,466 @@
/**
* Tests for CLI commands using mock backend.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Command } from "commander";
import { createMockBackend } from "./setup.js";
import type { Backend } from "../src/backend/base.js";
import { setAgentMode } from "../src/state.js";
let mockBackend: Backend;
// Capture console.log and console.error output
let output: string;
let errOutput: string;
const originalLog = console.log;
const originalError = console.error;
beforeEach(() => {
mockBackend = createMockBackend();
output = "";
errOutput = "";
console.log = (...args: unknown[]) => {
output += args.map(String).join(" ") + "\n";
};
console.error = (...args: unknown[]) => {
errOutput += args.map(String).join(" ") + "\n";
};
});
// Restore after each test
import { afterEach } from "vitest";
afterEach(() => {
console.log = originalLog;
console.error = originalError;
setAgentMode(false);
});
describe("cmdAdd", () => {
it("adds text memory", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "I prefer dark mode", {
userId: "alice",
immutable: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledOnce();
});
it("adds from messages JSON", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, undefined, {
userId: "alice",
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
immutable: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledOnce();
});
it("outputs json format", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
output: "json",
});
expect(output).toContain("results");
});
it("quiet mode produces no memory content", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
output: "quiet",
});
expect(output).not.toContain("dark mode");
});
});
describe("cmdAdd forwards --no-infer (regression for #5261)", () => {
it("forwards infer: false when --no-infer is set", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
// `infer: false` is the shape Commander produces for `--no-infer`.
await cmdAdd(mockBackend, "store me verbatim", {
userId: "alice",
immutable: false,
infer: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledWith(
"store me verbatim",
undefined,
expect.objectContaining({ infer: false }),
);
});
it("forwards infer: true by default (flag absent)", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "infer me", {
userId: "alice",
immutable: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledWith(
"infer me",
undefined,
expect.objectContaining({ infer: true }),
);
});
it("Commander stores --no-infer as opts.infer, not opts.noInfer", () => {
// Pins the assumption the fix relies on: Commander's `--no-X` option
// populates the positive camelCase key (`infer`), never `noInfer`.
const withFlag = new Command();
withFlag.option("--no-infer", "Skip inference, store raw.").action(() => {});
withFlag.parse(["--no-infer"], { from: "user" });
expect(withFlag.opts().infer).toBe(false);
expect(withFlag.opts().noInfer).toBeUndefined();
const withoutFlag = new Command();
withoutFlag.option("--no-infer", "Skip inference, store raw.").action(() => {});
withoutFlag.parse([], { from: "user" });
expect(withoutFlag.opts().infer).toBe(true);
});
});
describe("cmdAdd deduplicates PENDING", () => {
const DUPLICATE_PENDING = {
results: [
{ status: "PENDING", event_id: "evt-dup" },
{ status: "PENDING", event_id: "evt-dup" },
],
};
it("text shows one pending block", async () => {
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
output: "text",
});
expect(output.match(/Queued/g)?.length).toBe(1);
});
it("json shows one pending entry", async () => {
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
output: "json",
});
const data = JSON.parse(output);
const pending = data.results.filter((r: Record<string, unknown>) => r.status === "PENDING");
expect(pending).toHaveLength(1);
});
it("agent shows one pending entry", async () => {
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
setAgentMode(true);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
output: "agent",
});
const data = JSON.parse(output);
expect(data.count).toBe(1);
expect(data.data).toHaveLength(1);
});
});
describe("cmdSearch", () => {
it("searches and shows results in text mode", async () => {
const { cmdSearch } = await import("../src/commands/memory.js");
await cmdSearch(mockBackend, "preferences", {
userId: "alice",
topK: 10,
threshold: 0.3,
rerank: false,
keyword: false,
output: "text",
});
expect(output).toContain("Found 2");
});
it("outputs json format", async () => {
const { cmdSearch } = await import("../src/commands/memory.js");
await cmdSearch(mockBackend, "preferences", {
userId: "alice",
topK: 10,
threshold: 0.3,
rerank: false,
keyword: false,
output: "json",
});
expect(output).toContain("memory");
});
it("shows no results message", async () => {
(mockBackend.search as ReturnType<typeof vi.fn>).mockResolvedValue([]);
const { cmdSearch } = await import("../src/commands/memory.js");
await cmdSearch(mockBackend, "nonexistent", {
userId: "alice",
topK: 10,
threshold: 0.3,
rerank: false,
keyword: false,
output: "text",
});
expect(errOutput).toContain("No memories found");
});
});
describe("cmdGet", () => {
it("gets memory in text mode", async () => {
const { cmdGet } = await import("../src/commands/memory.js");
await cmdGet(mockBackend, "abc-123-def-456", { output: "text" });
expect(output).toContain("dark mode");
});
it("gets memory in json mode", async () => {
const { cmdGet } = await import("../src/commands/memory.js");
await cmdGet(mockBackend, "abc-123-def-456", { output: "json" });
expect(output).toContain("memory");
});
});
describe("cmdList", () => {
it("lists in table mode", async () => {
const { cmdList } = await import("../src/commands/memory.js");
await cmdList(mockBackend, {
userId: "alice",
page: 1,
pageSize: 100,
output: "table",
});
expect(output).toContain("dark mode");
});
it("shows empty message", async () => {
(mockBackend.listMemories as ReturnType<typeof vi.fn>).mockResolvedValue([]);
const { cmdList } = await import("../src/commands/memory.js");
await cmdList(mockBackend, {
userId: "alice",
page: 1,
pageSize: 100,
output: "text",
});
expect(errOutput).toContain("No memories found");
});
});
describe("cmdUpdate", () => {
it("updates memory", async () => {
const { cmdUpdate } = await import("../src/commands/memory.js");
await cmdUpdate(mockBackend, "abc-123", "New text", { output: "text" });
expect(output.toLowerCase()).toContain("updated");
});
});
describe("cmdDelete", () => {
it("deletes memory", async () => {
const { cmdDelete } = await import("../src/commands/memory.js");
await cmdDelete(mockBackend, "abc-123", { output: "text" });
expect(output.toLowerCase()).toContain("deleted");
});
});
describe("cmdDeleteAll", () => {
it("deletes all with force", async () => {
const { cmdDeleteAll } = await import("../src/commands/memory.js");
await cmdDeleteAll(mockBackend, {
force: true,
userId: "alice",
output: "text",
});
expect(output.toLowerCase()).toContain("deleted");
});
});
describe("cmdEntitiesList", () => {
it("lists users in table mode", async () => {
const { cmdEntitiesList } = await import("../src/commands/entities.js");
await cmdEntitiesList(mockBackend, "users", { output: "table" });
expect(output).toContain("alice");
});
it("lists in json mode", async () => {
const { cmdEntitiesList } = await import("../src/commands/entities.js");
await cmdEntitiesList(mockBackend, "users", { output: "json" });
expect(output).toContain("alice");
});
});
describe("cmdEventList", () => {
it("lists events in table mode", async () => {
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "table" });
expect(output).toContain("evt-abc-");
expect(output).toContain("ADD");
expect(output).toContain("SUCCEEDED");
});
it("lists events in json mode", async () => {
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "json" });
expect(output).toContain("evt-abc-123-def-456");
expect(output).toContain("evt-def-456-ghi-789");
});
it("shows empty message when no events", async () => {
(mockBackend.listEvents as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "table" });
expect((output + errOutput).toLowerCase()).toContain("no events");
});
});
describe("cmdEventStatus", () => {
it("shows event details in text mode", async () => {
const { cmdEventStatus } = await import("../src/commands/events.js");
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "text" });
expect(output).toContain("evt-abc-123-def-456");
expect(output).toContain("SUCCEEDED");
});
it("shows event details in json mode", async () => {
const { cmdEventStatus } = await import("../src/commands/events.js");
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "json" });
expect(output).toContain("evt-abc-123-def-456");
expect(output).toContain("ADD");
});
});
describe("agent mode", () => {
it("cmdAdd outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test preference", {
userId: "alice",
immutable: false,
output: "agent",
});
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("add");
expect(parsed.data).toBeDefined();
expect(parsed.scope).toMatchObject({ user_id: "alice" });
expect(Object.keys(parsed.data[0]).sort()).toEqual(["event", "id", "memory"].sort());
});
it("cmdSearch outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdSearch } = await import("../src/commands/memory.js");
await cmdSearch(mockBackend, "preferences", {
userId: "alice",
topK: 10,
threshold: 0.3,
rerank: false,
keyword: false,
output: "agent",
});
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("search");
expect(Array.isArray(parsed.data)).toBe(true);
expect(parsed.count).toBe(2);
const keys = Object.keys(parsed.data[0]);
expect(keys).toContain("id");
expect(keys).toContain("memory");
expect(keys).toContain("score");
expect(keys).toContain("created_at");
expect(keys).toContain("categories");
expect(keys).not.toContain("user_id");
expect(keys).not.toContain("agent_id");
});
it("cmdList outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdList } = await import("../src/commands/memory.js");
await cmdList(mockBackend, {
userId: "alice",
page: 1,
pageSize: 100,
output: "agent",
});
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("list");
expect(Array.isArray(parsed.data)).toBe(true);
expect(parsed.count).toBe(2);
expect(Object.keys(parsed.data[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
});
it("cmdGet outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdGet } = await import("../src/commands/memory.js");
await cmdGet(mockBackend, "abc-123-def-456", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("get");
expect(parsed.data).toBeDefined();
expect(parsed.data).toMatchObject({ id: "abc-123-def-456" });
expect(Object.keys(parsed.data)).not.toContain("user_id");
});
it("cmdUpdate outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdUpdate } = await import("../src/commands/memory.js");
await cmdUpdate(mockBackend, "abc-123", "Updated text", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("update");
expect(parsed.data).toBeDefined();
});
it("cmdDelete outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdDelete } = await import("../src/commands/memory.js");
await cmdDelete(mockBackend, "abc-123", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("delete");
expect(parsed.data).toBeDefined();
});
it("cmdEventList outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("event list");
expect(Array.isArray(parsed.data)).toBe(true);
expect(parsed.count).toBe(2);
expect(Object.keys(parsed.data[0]).sort()).toEqual(
["created_at", "event_type", "id", "latency", "status"],
);
expect(Object.keys(parsed.data[0])).not.toContain("updated_at");
});
it("cmdEventStatus outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdEventStatus } = await import("../src/commands/events.js");
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("event status");
expect(parsed.data).toBeDefined();
expect(parsed.data).toMatchObject({ id: "evt-abc-123-def-456" });
expect(parsed.data.results[0]).toHaveProperty("memory");
expect(parsed.data.results[0]).not.toHaveProperty("data");
});
});
+107
View File
@@ -0,0 +1,107 @@
/**
* Tests for configuration management.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
createDefaultConfig,
loadConfig,
saveConfig,
redactKey,
getNestedValue,
setNestedValue,
CONFIG_DIR,
CONFIG_FILE,
} from "../src/config.js";
// Use a temp directory for config during tests
let origConfigDir: string;
let origConfigFile: string;
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
// Monkey-patch the module-level constants
// We'll use env vars and direct file manipulation instead
// Clear MEM0_ env vars
for (const key of Object.keys(process.env)) {
if (key.startsWith("MEM0_")) {
delete process.env[key];
}
}
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe("redactKey", () => {
it("returns '(not set)' for empty key", () => {
expect(redactKey("")).toBe("(not set)");
});
it("redacts short key", () => {
expect(redactKey("abc")).toBe("ab***");
});
it("redacts normal key", () => {
const result = redactKey("m0-abcdefgh12345678");
expect(result).toBe("m0-a...5678");
expect(result).not.toContain("abcdefgh");
});
it("redacts exactly 8-char key as short", () => {
expect(redactKey("12345678")).toBe("12***");
});
});
describe("createDefaultConfig", () => {
it("has correct defaults", () => {
const config = createDefaultConfig();
expect(config.platform.baseUrl).toBe("https://api.mem0.ai");
expect(config.platform.apiKey).toBe("");
expect(config.defaults.userId).toBe("");
});
});
describe("getNestedValue", () => {
it("gets platform.api_key", () => {
const config = createDefaultConfig();
config.platform.apiKey = "test-key";
expect(getNestedValue(config, "platform.api_key")).toBe("test-key");
});
it("returns undefined for nonexistent key", () => {
const config = createDefaultConfig();
expect(getNestedValue(config, "nonexistent.key")).toBeUndefined();
});
it("gets defaults.user_id", () => {
const config = createDefaultConfig();
config.defaults.userId = "alice";
expect(getNestedValue(config, "defaults.user_id")).toBe("alice");
});
});
describe("setNestedValue", () => {
it("sets platform.api_key", () => {
const config = createDefaultConfig();
expect(setNestedValue(config, "platform.api_key", "new-key")).toBe(true);
expect(config.platform.apiKey).toBe("new-key");
});
it("returns false for nonexistent key", () => {
const config = createDefaultConfig();
expect(setNestedValue(config, "nonexistent.key", "val")).toBe(false);
});
it("sets defaults.user_id", () => {
const config = createDefaultConfig();
expect(setNestedValue(config, "defaults.user_id", "bob")).toBe(true);
expect(config.defaults.userId).toBe("bob");
});
});
+168
View File
@@ -0,0 +1,168 @@
/**
* Unit tests for init internals — decision tree primitives + plugin sync.
*
* Mirror of `cli/python/tests/test_init_internals.py`. Both files MUST stay
* in sync — if you add a behavioral assertion here, mirror it on the Python
* side and vice versa.
*
* - `pingKey` must NOT treat network errors as "invalid key" (else a VPN
* flap silently mints a new shadow over a working key).
* - `plugin_sync` must only update entries that already exist, preserve
* trailing newlines, and never mangle other lines.
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { pingKey } from "../src/commands/init.js";
import { updateClaudeSettings, updateShellRc } from "../src/plugin-sync.js";
// ── pingKey ──────────────────────────────────────────────────────────────
describe("pingKey — network vs auth distinction", () => {
const origFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = origFetch;
vi.restoreAllMocks();
});
it("returns true for 200", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 } as Response);
await expect(pingKey("k", "http://x")).resolves.toBe(true);
});
it("returns false for 401 (definitively invalid)", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ status: 401 } as Response);
await expect(pingKey("k", "http://x")).resolves.toBe(false);
});
it("returns false for 403 (definitively invalid)", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ status: 403 } as Response);
await expect(pingKey("k", "http://x")).resolves.toBe(false);
});
it("returns true for 5xx (transient upstream — prefer reuse)", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ status: 503 } as Response);
await expect(pingKey("k", "http://x")).resolves.toBe(true);
});
it("returns true on network error (prefer reuse over re-mint)", async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
await expect(pingKey("k", "http://x")).resolves.toBe(true);
});
it("returns true on timeout (prefer reuse)", async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error("aborted"));
await expect(pingKey("k", "http://x")).resolves.toBe(true);
});
});
// ── updateShellRc ────────────────────────────────────────────────────────
describe("updateShellRc — exists-only contract", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("updates existing export and preserves trailing newline", () => {
const rc = path.join(tmpDir, ".zshrc");
fs.writeFileSync(rc, 'export MEM0_API_KEY="old"\n');
expect(updateShellRc(rc, "newkey")).toBe(true);
expect(fs.readFileSync(rc, "utf-8")).toBe('export MEM0_API_KEY="newkey"\n');
});
it("does NOT create a new export when none exists", () => {
const rc = path.join(tmpDir, ".zshrc");
fs.writeFileSync(rc, "alias ll='ls -la'\n");
expect(updateShellRc(rc, "newkey")).toBe(false);
expect(fs.readFileSync(rc, "utf-8")).toBe("alias ll='ls -la'\n");
});
it("preserves surrounding content", () => {
const rc = path.join(tmpDir, ".zshrc");
const original =
"# my zshrc\n" +
"alias ll='ls -la'\n" +
"export MEM0_API_KEY='old'\n" +
"export OTHER=keepme\n";
fs.writeFileSync(rc, original);
updateShellRc(rc, "newkey");
const after = fs.readFileSync(rc, "utf-8");
expect(after).toContain("alias ll='ls -la'\n");
expect(after).toContain("export OTHER=keepme\n");
expect(after).toContain("# my zshrc\n");
expect(after).toContain('export MEM0_API_KEY="newkey"\n');
});
it("is idempotent when value already matches", () => {
const rc = path.join(tmpDir, ".zshrc");
fs.writeFileSync(rc, 'export MEM0_API_KEY="same"\n');
expect(updateShellRc(rc, "same")).toBe(false);
});
it("is a no-op for missing files", () => {
const rc = path.join(tmpDir, ".zshrc"); // does not exist
expect(updateShellRc(rc, "x")).toBe(false);
});
});
// ── updateClaudeSettings ─────────────────────────────────────────────────
describe("updateClaudeSettings — never creates entries", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("does not create env block when none exists", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(settings, JSON.stringify({ otherKey: 1 }));
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
expect(JSON.parse(fs.readFileSync(settings, "utf-8"))).toEqual({
otherKey: 1,
});
});
it("does not create MEM0_API_KEY entry in existing env block", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(settings, JSON.stringify({ env: { OTHER_KEY: "x" } }));
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
});
it("updates existing entry and preserves siblings", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(
settings,
JSON.stringify({ env: { MEM0_API_KEY: "old", OTHER: "y" } }, null, 2),
);
expect(updateClaudeSettings(settings, "fresh")).toBe(true);
const data = JSON.parse(fs.readFileSync(settings, "utf-8"));
expect(data.env.MEM0_API_KEY).toBe("fresh");
expect(data.env.OTHER).toBe("y");
});
it("is idempotent when value already matches", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(
settings,
JSON.stringify({ env: { MEM0_API_KEY: "same" } }),
);
expect(updateClaudeSettings(settings, "same")).toBe(false);
});
it("is a no-op for malformed JSON", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(settings, "{ this is not json");
expect(updateClaudeSettings(settings, "x")).toBe(false);
});
});
+198
View File
@@ -0,0 +1,198 @@
/**
* Tests for output formatting.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
formatMemoriesText,
formatMemoriesTable,
formatJson,
formatSingleMemory,
formatAddResult,
printResultSummary,
sanitizeAgentData,
} from "../src/output.js";
let output: string;
const originalLog = console.log;
beforeEach(() => {
output = "";
console.log = (...args: unknown[]) => {
output += args.map(String).join(" ") + "\n";
};
});
afterEach(() => {
console.log = originalLog;
});
const sampleMemories = [
{
id: "abc-123-def-456",
memory: "User prefers dark mode",
score: 0.92,
created_at: "2026-02-15T10:30:00Z",
categories: ["preferences"],
},
{
id: "ghi-789-jkl-012",
memory: "User uses vim keybindings",
score: 0.78,
created_at: "2026-03-01T14:00:00Z",
categories: ["tools"],
},
];
describe("formatMemoriesText", () => {
it("shows count and memory content", () => {
formatMemoriesText(sampleMemories);
expect(output).toContain("Found 2");
expect(output).toContain("dark mode");
expect(output).toContain("vim keybindings");
});
it("shows scores and IDs", () => {
formatMemoriesText(sampleMemories);
expect(output).toContain("0.92");
expect(output).toContain("abc-123-");
});
});
describe("formatMemoriesTable", () => {
it("renders a table with memory content", () => {
formatMemoriesTable(sampleMemories);
expect(output).toContain("dark mode");
});
});
describe("formatJson", () => {
it("outputs valid JSON", () => {
formatJson({ key: "value" });
expect(JSON.parse(output)).toEqual({ key: "value" });
});
});
describe("formatSingleMemory", () => {
it("shows memory text in text mode", () => {
formatSingleMemory(sampleMemories[0], "text");
expect(output).toContain("dark mode");
});
it("outputs JSON in json mode", () => {
formatSingleMemory(sampleMemories[0], "json");
expect(output).toContain("memory");
});
});
describe("formatAddResult", () => {
it("shows ADD event", () => {
formatAddResult({
results: [{ id: "abc-123", memory: "Test", event: "ADD" }],
});
expect(output).toContain("Added");
});
it("shows PENDING event", () => {
formatAddResult({
results: [{ status: "PENDING", event_id: "evt-12345678" }],
});
expect(output).toContain("Queued");
});
it("deduplicates PENDING entries with same event_id", () => {
formatAddResult({
results: [
{ status: "PENDING", event_id: "evt-dup" },
{ status: "PENDING", event_id: "evt-dup" },
],
});
// Should show only one PENDING block despite two entries with same event_id
expect(output.match(/Queued/g)?.length).toBe(1);
expect(output.match(/evt-dup/g)?.length).toBe(2); // event_id line + status hint line
});
});
describe("printResultSummary", () => {
it("shows count and duration", () => {
printResultSummary({ count: 5, durationSecs: 1.23 });
expect(output).toContain("5 results");
expect(output).toContain("1.23s");
});
it("handles singular", () => {
printResultSummary({ count: 1 });
expect(output).toContain("1 result");
expect(output).not.toContain("results");
});
});
describe("sanitizeAgentData", () => {
it("projects add results", () => {
const raw = [{ id: "abc", memory: "test", event: "ADD", metadata: { x: 1 }, categories: ["a"] }];
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
expect(result).toEqual([{ id: "abc", memory: "test", event: "ADD" }]);
});
it("passes through PENDING add items", () => {
const raw = [{ status: "PENDING", event_id: "evt-123", noise: "x" }];
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
expect(result).toEqual([{ status: "PENDING", event_id: "evt-123" }]);
});
it("projects search results", () => {
const raw = [{ id: "abc", memory: "test", score: 0.9, created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
const result = sanitizeAgentData("search", raw) as Record<string, unknown>[];
expect(result[0]).not.toHaveProperty("user_id");
expect(result[0]).toHaveProperty("score");
});
it("projects list results", () => {
const raw = [{ id: "abc", memory: "test", created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
const result = sanitizeAgentData("list", raw) as Record<string, unknown>[];
expect(Object.keys(result[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
});
it("projects get result", () => {
const raw = { id: "abc", memory: "test", created_at: "2026-01-01", updated_at: "2026-01-02", categories: ["a"], metadata: { k: "v" }, user_id: "u1" };
const result = sanitizeAgentData("get", raw) as Record<string, unknown>;
expect(result).not.toHaveProperty("user_id");
expect(result).toHaveProperty("metadata");
});
it("projects update result", () => {
const raw = { id: "abc", memory: "updated", extra: "noise" };
const result = sanitizeAgentData("update", raw);
expect(result).toEqual({ id: "abc", memory: "updated" });
});
it("projects event list results", () => {
const raw = [{ id: "evt-1", event_type: "ADD", status: "SUCCEEDED", graph_status: null, latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02" }];
const result = sanitizeAgentData("event list", raw) as Record<string, unknown>[];
expect(result[0]).not.toHaveProperty("updated_at");
expect(result[0]).not.toHaveProperty("graph_status");
});
it("flattens event status results", () => {
const raw = {
id: "evt-1", event_type: "ADD", status: "SUCCEEDED",
latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02",
results: [{ id: "mem-1", event: "ADD", user_id: "alice", data: { memory: "dark mode" } }],
};
const result = sanitizeAgentData("event status", raw) as Record<string, unknown>;
const firstResult = (result.results as Record<string, unknown>[])[0];
expect(firstResult).toHaveProperty("memory", "dark mode");
expect(firstResult).not.toHaveProperty("data");
});
it("passes through status/config/import commands unchanged", () => {
const data = { key: "value", other: "stuff" };
for (const cmd of ["status", "import", "config show", "config get", "config set"]) {
expect(sanitizeAgentData(cmd, data)).toEqual(data);
}
});
it("handles null data", () => {
expect(sanitizeAgentData("add", null)).toBeNull();
});
});
+100
View File
@@ -0,0 +1,100 @@
/**
* Tests for the Platform backend (mem0 Platform API client).
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PlatformBackend } from "../src/backend/platform.js";
import { createDefaultConfig } from "../src/config.js";
function makeBackend(): PlatformBackend {
// apiKey/baseUrl only build request headers; every test spies on _request,
// so no real network calls are made.
return new PlatformBackend(createDefaultConfig().platform);
}
function mockFetch() {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: vi.fn().mockReturnValue(null) },
json: vi.fn().mockResolvedValue({ message: "ok" }),
});
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("deleteEntities", () => {
it("returns all results keyed by entity type for a multi-entity delete", async () => {
const backend = makeBackend();
const responses: Record<string, unknown> = {
"/v2/entities/user/alice/": { message: "user deleted" },
"/v2/entities/agent/bob/": { message: "agent deleted" },
};
const spy = vi
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
.spyOn(backend as any, "_request")
.mockImplementation(async (_method: string, path: string) => responses[path]);
const result = await backend.deleteEntities({ userId: "alice", agentId: "bob" });
// Regression: previously only the last entity's response survived.
expect(result).toEqual({
user: { message: "user deleted" },
agent: { message: "agent deleted" },
});
expect(spy).toHaveBeenCalledTimes(2);
});
it("keys a single-entity delete by its type", async () => {
const backend = makeBackend();
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
vi.spyOn(backend as any, "_request").mockResolvedValue({ message: "user deleted" });
const result = await backend.deleteEntities({ userId: "alice" });
expect(result).toEqual({ user: { message: "user deleted" } });
});
it("throws when no entity id is provided", async () => {
const backend = makeBackend();
await expect(backend.deleteEntities({})).rejects.toThrow(
"At least one entity ID is required",
);
});
});
describe("PlatformBackend path encoding", () => {
it("encodes memory IDs before interpolating them into paths", async () => {
const fetchMock = mockFetch();
const backend = makeBackend();
await backend.get("mem/a?b#c");
await backend.update("mem/a?b#c", "updated");
await backend.delete("mem/a?b#c");
const urls = fetchMock.mock.calls.map((call) => call[0]);
expect(urls).toEqual([
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/",
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
]);
});
it("encodes entity and event IDs before interpolating them into paths", async () => {
const fetchMock = mockFetch();
const backend = makeBackend();
await backend.deleteEntities({ userId: "org/team?active#frag" });
await backend.getEvent("evt/a?b#c");
const urls = fetchMock.mock.calls.map((call) => call[0]);
expect(urls).toEqual([
"https://api.mem0.ai/v2/entities/user/org%2Fteam%3Factive%23frag/?source=CLI",
"https://api.mem0.ai/v1/event/evt%2Fa%3Fb%23c/",
]);
});
});
+112
View File
@@ -0,0 +1,112 @@
/**
* Shared test helpers and mock factories for mem0 CLI tests.
*/
import { vi } from "vitest";
import type { Backend } from "../src/backend/base.js";
/** Create a mock backend with all methods stubbed with sensible defaults. */
export function createMockBackend(): Backend {
return {
add: vi.fn().mockResolvedValue({
results: [
{
id: "abc-123-def-456",
memory: "User prefers dark mode",
event: "ADD",
},
],
}),
search: vi.fn().mockResolvedValue([
{
id: "abc-123-def-456",
memory: "User prefers dark mode",
score: 0.92,
created_at: "2026-02-15T10:30:00Z",
categories: ["preferences"],
},
{
id: "ghi-789-jkl-012",
memory: "User uses vim keybindings",
score: 0.78,
created_at: "2026-03-01T14:00:00Z",
categories: ["tools"],
},
]),
get: vi.fn().mockResolvedValue({
id: "abc-123-def-456",
memory: "User prefers dark mode",
created_at: "2026-02-15T10:30:00Z",
updated_at: "2026-02-20T08:00:00Z",
metadata: { source: "onboarding" },
categories: ["preferences"],
}),
listMemories: vi.fn().mockResolvedValue([
{
id: "abc-123-def-456",
memory: "User prefers dark mode",
created_at: "2026-02-15T10:30:00Z",
categories: ["preferences"],
},
{
id: "ghi-789-jkl-012",
memory: "User uses vim keybindings",
created_at: "2026-03-01T14:00:00Z",
categories: ["tools"],
},
]),
update: vi.fn().mockResolvedValue({ id: "abc-123-def-456", memory: "Updated memory" }),
delete: vi.fn().mockResolvedValue({ status: "deleted" }),
status: vi.fn().mockResolvedValue({
connected: true,
backend: "platform",
base_url: "https://api.mem0.ai",
}),
deleteEntities: vi.fn().mockResolvedValue({ message: "Entity deleted" }),
entities: vi.fn().mockResolvedValue([
{ name: "alice", count: 5 },
{ name: "bob", count: 3 },
]),
listEvents: vi.fn().mockResolvedValue([
{
id: "evt-abc-123-def-456",
event_type: "ADD",
status: "SUCCEEDED",
graph_status: null,
latency: 1234.5,
created_at: "2026-04-01T10:00:00Z",
updated_at: "2026-04-01T10:00:01Z",
},
{
id: "evt-def-456-ghi-789",
event_type: "SEARCH",
status: "PENDING",
graph_status: null,
latency: null,
created_at: "2026-04-01T10:01:00Z",
updated_at: "2026-04-01T10:01:00Z",
},
]),
getEvent: vi.fn().mockResolvedValue({
id: "evt-abc-123-def-456",
event_type: "ADD",
status: "SUCCEEDED",
graph_status: "SUCCEEDED",
latency: 1234.5,
created_at: "2026-04-01T10:00:00Z",
updated_at: "2026-04-01T10:00:01Z",
results: [
{
id: "mem-abc-123",
event: "ADD",
user_id: "alice",
data: { memory: "User prefers dark mode" },
},
],
}),
};
}
+59
View File
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockLoadConfig = vi.fn();
const mockSaveConfig = vi.fn();
const mockSpawn = vi.fn();
vi.mock("../src/config.js", () => ({
CONFIG_FILE: "/tmp/mem0-config.json",
loadConfig: mockLoadConfig,
saveConfig: mockSaveConfig,
}));
vi.mock("node:child_process", () => ({
spawn: mockSpawn,
}));
describe("captureEvent", () => {
beforeEach(() => {
vi.resetModules();
mockLoadConfig.mockReset();
mockSaveConfig.mockReset();
mockSpawn.mockReset();
delete process.env.MEM0_TELEMETRY;
});
it("pipes the telemetry context through stdin instead of argv", async () => {
mockLoadConfig.mockReturnValue({
platform: {
apiKey: "m0-node-secret",
baseUrl: "https://api.mem0.ai",
userEmail: "",
},
telemetry: {
anonymousId: "cli-anon-node",
},
});
const stdin = { end: vi.fn() };
const child = { stdin, unref: vi.fn() };
mockSpawn.mockReturnValue(child);
const { captureEvent } = await import("../src/telemetry.js");
captureEvent("node_test_event", { case: "stdin-secret" });
expect(mockSpawn).toHaveBeenCalledTimes(1);
const [execPath, args, options] = mockSpawn.mock.calls[0];
expect(execPath).toBe(process.execPath);
expect(args).toHaveLength(1);
expect(String(args[0])).toContain("telemetry-sender.cjs");
expect(JSON.stringify(args)).not.toContain("m0-node-secret");
expect(options).toMatchObject({ detached: true, stdio: ["pipe", "ignore", "ignore"] });
expect(stdin.end).toHaveBeenCalledTimes(1);
const payload = JSON.parse(stdin.end.mock.calls[0][0]);
expect(payload.mem0ApiKey).toBe("m0-node-secret");
expect(payload.payload.event).toBe("node_test_event");
expect(child.unref).toHaveBeenCalledTimes(1);
});
});