From 458576dd0c8834fda320c32662ba18c5ec433b3f Mon Sep 17 00:00:00 2001 From: kartik-mem0 Date: Sun, 5 Apr 2026 01:36:31 +0530 Subject: [PATCH] test: add unit tests for CLI commands, telemetry, and fs-safe utilities --- openclaw/tests/cli-commands.test.ts | 181 ++++++++++++++++++++++++++++ openclaw/tests/fs-safe.test.ts | 37 ++++++ openclaw/tests/telemetry.test.ts | 65 ++++++++++ 3 files changed, 283 insertions(+) create mode 100644 openclaw/tests/fs-safe.test.ts create mode 100644 openclaw/tests/telemetry.test.ts diff --git a/openclaw/tests/cli-commands.test.ts b/openclaw/tests/cli-commands.test.ts index 12baa26f3..ae9f06d44 100644 --- a/openclaw/tests/cli-commands.test.ts +++ b/openclaw/tests/cli-commands.test.ts @@ -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).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).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).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).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).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).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"), + ); + }); + }); }); diff --git a/openclaw/tests/fs-safe.test.ts b/openclaw/tests/fs-safe.test.ts new file mode 100644 index 000000000..29b6eddfe --- /dev/null +++ b/openclaw/tests/fs-safe.test.ts @@ -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"); + }); +}); diff --git a/openclaw/tests/telemetry.test.ts b/openclaw/tests/telemetry.test.ts new file mode 100644 index 000000000..23ddd51ef --- /dev/null +++ b/openclaw/tests/telemetry.test.ts @@ -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; + + 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).mockReturnValueOnce({ + userEmail: "test@example.com", + }); + expect(() => captureEvent("test_event")).not.toThrow(); + }); + + it("falls back to anonymous-openclaw when no apiKey", () => { + (readPluginAuth as ReturnType).mockReturnValueOnce({}); + expect(() => captureEvent("test_event", {}, {})).not.toThrow(); + }); + + it("handles readPluginAuth errors gracefully", () => { + (readPluginAuth as ReturnType).mockImplementationOnce(() => { + throw new Error("config read failed"); + }); + expect(() => captureEvent("test_event")).not.toThrow(); + }); +});