test: add unit tests for CLI commands, telemetry, and fs-safe utilities
This commit is contained in:
@@ -153,6 +153,18 @@ function createMockBackend() {
|
||||
connected: true,
|
||||
url: "https://api.mem0.ai",
|
||||
}),
|
||||
add: vi.fn().mockResolvedValue({ id: "new-1" }),
|
||||
listEvents: vi.fn().mockResolvedValue([
|
||||
{ id: "evt-1111-2222-3333-4444", event_type: "ADD", status: "SUCCEEDED", latency: 1500, created_at: "2026-04-01T12:00:00Z" },
|
||||
]),
|
||||
getEvent: vi.fn().mockResolvedValue({
|
||||
id: "evt-1111-2222-3333-4444",
|
||||
event_type: "ADD",
|
||||
status: "SUCCEEDED",
|
||||
latency: 1500,
|
||||
created_at: "2026-04-01T12:00:00Z",
|
||||
updated_at: "2026-04-01T12:00:01Z",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1232,4 +1244,173 @@ describe("registerCliCommands", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// import subcommand
|
||||
// ========================================================================
|
||||
|
||||
describe("import subcommand", () => {
|
||||
it("imports memories from a JSON array file", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify([
|
||||
{ memory: "fact one" },
|
||||
{ memory: "fact two" },
|
||||
]),
|
||||
);
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("memories.json", {});
|
||||
|
||||
expect(backend.add).toHaveBeenCalledTimes(2);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 2 memories.");
|
||||
});
|
||||
|
||||
it("imports a single JSON object", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify({ memory: "single fact" }),
|
||||
);
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("single.json", {});
|
||||
|
||||
expect(backend.add).toHaveBeenCalledTimes(1);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 1 memories.");
|
||||
});
|
||||
|
||||
it("skips items with no extractable content", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify([{ memory: "valid" }, { nofield: true }]),
|
||||
);
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("mixed.json", {});
|
||||
|
||||
expect(backend.add).toHaveBeenCalledTimes(1);
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import.");
|
||||
});
|
||||
|
||||
it("uses --user-id and --agent-id overrides", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify([{ memory: "fact" }]),
|
||||
);
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("f.json", { userId: "override-user", agentId: "agent-1" });
|
||||
|
||||
expect(backend.add).toHaveBeenCalledWith("fact", undefined, expect.objectContaining({
|
||||
userId: "override-user",
|
||||
agentId: "agent-1",
|
||||
}));
|
||||
});
|
||||
|
||||
it("handles file read errors gracefully", async () => {
|
||||
const { mem0 } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("missing.json", {});
|
||||
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to read file"),
|
||||
);
|
||||
});
|
||||
|
||||
it("handles backend.add failures gracefully", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify([{ memory: "will fail" }]),
|
||||
);
|
||||
backend.add.mockRejectedValueOnce(new Error("API error"));
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("fail.json", {});
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 0 memories.");
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import.");
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// event subcommand
|
||||
// ========================================================================
|
||||
|
||||
describe("event subcommand", () => {
|
||||
it("lists events in table format", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const listCmd = findCommand(eventCmd, "list")!;
|
||||
|
||||
await listCmd._action!();
|
||||
|
||||
expect(backend.listEvents).toHaveBeenCalled();
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("evt-1111-2222-3333-4444"),
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("1 event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("prints message when no events found", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
backend.listEvents.mockResolvedValueOnce([]);
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const listCmd = findCommand(eventCmd, "list")!;
|
||||
|
||||
await listCmd._action!();
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("No events found.");
|
||||
});
|
||||
|
||||
it("handles event list errors gracefully", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
backend.listEvents.mockRejectedValueOnce(new Error("event boom"));
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const listCmd = findCommand(eventCmd, "list")!;
|
||||
|
||||
await listCmd._action!();
|
||||
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to list events"),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows event status details", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const statusCmd = findCommand(eventCmd, "status")!;
|
||||
|
||||
await statusCmd._action!("evt-1111-2222-3333-4444");
|
||||
|
||||
expect(backend.getEvent).toHaveBeenCalledWith("evt-1111-2222-3333-4444");
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Event ID: evt-1111-2222-3333-4444");
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Type: ADD");
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Status: SUCCEEDED");
|
||||
});
|
||||
|
||||
it("handles event status errors gracefully", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
backend.getEvent.mockRejectedValueOnce(new Error("not found"));
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const statusCmd = findCommand(eventCmd, "status")!;
|
||||
|
||||
await statusCmd._action!("bad-id");
|
||||
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to get event"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { bootstrapTelemetryFlag } from "../fs-safe.ts";
|
||||
|
||||
describe("bootstrapTelemetryFlag", () => {
|
||||
const originalEnv = process.env.MEM0_TELEMETRY;
|
||||
|
||||
beforeEach(() => {
|
||||
delete (globalThis as any).__mem0_telemetry_override;
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (globalThis as any).__mem0_telemetry_override;
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.MEM0_TELEMETRY = originalEnv;
|
||||
} else {
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
}
|
||||
});
|
||||
|
||||
it("sets globalThis override when MEM0_TELEMETRY is set", () => {
|
||||
process.env.MEM0_TELEMETRY = "false";
|
||||
bootstrapTelemetryFlag();
|
||||
expect((globalThis as any).__mem0_telemetry_override).toBe("false");
|
||||
});
|
||||
|
||||
it("does not set globalThis override when MEM0_TELEMETRY is unset", () => {
|
||||
bootstrapTelemetryFlag();
|
||||
expect((globalThis as any).__mem0_telemetry_override).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes through truthy values", () => {
|
||||
process.env.MEM0_TELEMETRY = "true";
|
||||
bootstrapTelemetryFlag();
|
||||
expect((globalThis as any).__mem0_telemetry_override).toBe("true");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
// Mock config-file before importing telemetry
|
||||
vi.mock("../cli/config-file.ts", () => ({
|
||||
readPluginAuth: vi.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
import { captureEvent, PLUGIN_VERSION } from "../telemetry.ts";
|
||||
import { readPluginAuth } from "../cli/config-file.ts";
|
||||
|
||||
describe("telemetry", () => {
|
||||
let fetchSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset telemetry enabled state
|
||||
(globalThis as any).__mem0_telemetry_override = undefined;
|
||||
fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete (globalThis as any).__mem0_telemetry_override;
|
||||
});
|
||||
|
||||
it("exports PLUGIN_VERSION", () => {
|
||||
expect(PLUGIN_VERSION).toBe("1.0.4");
|
||||
});
|
||||
|
||||
it("captureEvent does not throw", () => {
|
||||
expect(() => captureEvent("test_event")).not.toThrow();
|
||||
});
|
||||
|
||||
it("captureEvent accepts properties and context", () => {
|
||||
expect(() =>
|
||||
captureEvent("test_event", { key: "val" }, { apiKey: "m0-key", mode: "platform" }),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("captureEvent is silent when telemetry disabled", () => {
|
||||
(globalThis as any).__mem0_telemetry_override = "false";
|
||||
// Force re-evaluation by resetting cached value
|
||||
// Since _telemetryEnabled is module-level, we test indirectly
|
||||
expect(() => captureEvent("test_event")).not.toThrow();
|
||||
});
|
||||
|
||||
it("uses userEmail as distinct ID when available", () => {
|
||||
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({
|
||||
userEmail: "test@example.com",
|
||||
});
|
||||
expect(() => captureEvent("test_event")).not.toThrow();
|
||||
});
|
||||
|
||||
it("falls back to anonymous-openclaw when no apiKey", () => {
|
||||
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({});
|
||||
expect(() => captureEvent("test_event", {}, {})).not.toThrow();
|
||||
});
|
||||
|
||||
it("handles readPluginAuth errors gracefully", () => {
|
||||
(readPluginAuth as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
||||
throw new Error("config read failed");
|
||||
});
|
||||
expect(() => captureEvent("test_event")).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user