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,
|
connected: true,
|
||||||
url: "https://api.mem0.ai",
|
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