Files
mem0/integrations/mem0-plugin/.opencode-plugin/telemetry.test.ts
kartik-mem0 013718c817 refactor(opencode): native SDK tools, drop MCP, trim skills, expand telemetry
Reworks @mem0/opencode-plugin to expose memory operations as native OpenCode
tools (via the @opencode-ai/plugin `tool()` helper, backed by the mem0ai SDK)
instead of delegating to the remote MCP server. Skills load via the `config`
hook (`skills.paths`) instead of being copied into the project `.opencode/`.

- Drop MCP: remove the regex-based MCP tool interception, the bundled
  opencode.json MCP registration, and the cli.ts installer (mem0-opencode bin).
- Native tools: add_memory, search_memories, get_memories, get_memory,
  update_memory, delete_memory, delete_all_memories, delete_entities,
  list_entities, get_event_status.
- Trim skills 16 -> 8 (context-loader, dream, forget, health, peek, pin,
  remember, tour); remove import, export, memory-reviewer, mem0, list-projects,
  switch-project, stats, onboard. De-MCP/onboard wording in kept skills.
- Telemetry: emit the full shared plugin.* schema (adds user_prompt, bash_error,
  pre_compact, session_stop alongside session_start, tool_use); tool_use now
  fires from inside each native tool; add project_hash + os_version props.
- Fix duplicate error-search (single topK:6); correct the documented
  experimental.chat.messages.transform hook name.
- Bump to 0.2.0.
2026-06-16 12:35:58 +05:30

79 lines
3.2 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test";
import { buildEvent, captureEvent, isTelemetryEnabled } from "./telemetry";
const KEY = "m0-testkey123";
afterEach(() => {
delete process.env.MEM0_TELEMETRY;
});
describe("opencode telemetry", () => {
test("buildEvent uses the shared plugin.* schema with platform=opencode", () => {
const payload = buildEvent("session_start", { memory_count: 5 }, KEY);
expect(payload).not.toBeNull();
const props = payload!.properties as Record<string, unknown>;
expect(payload!.event).toBe("plugin.session_start");
expect(props.source).toBe("plugin");
expect(props.platform).toBe("opencode");
expect(props.memory_count).toBe(5);
expect(props.$process_person_profile).toBe(false);
expect(typeof props.plugin_version).toBe("string");
});
test("distinct_id is sha256(apiKey)[:32] — matches the editor plugin", async () => {
const { createHash } = await import("node:crypto");
const expected = createHash("sha256").update(KEY).digest("hex").slice(0, 32);
expect(buildEvent("session_start", {}, KEY)!.distinct_id).toBe(expected);
});
test("system properties win over caller-supplied ones", () => {
const props = buildEvent("x", { platform: "HACK", source: "HACK" }, KEY)!
.properties as Record<string, unknown>;
expect(props.platform).toBe("opencode");
expect(props.source).toBe("plugin");
});
test("returns null without an API key (no anonymous events)", () => {
expect(buildEvent("session_start", {}, undefined)).toBeNull();
});
test("opt-out via MEM0_TELEMETRY disables events", () => {
process.env.MEM0_TELEMETRY = "false";
expect(isTelemetryEnabled()).toBe(false);
expect(buildEvent("session_start", {}, KEY)).toBeNull();
});
test("captureEvent never throws (and sends nothing when opted out)", () => {
process.env.MEM0_TELEMETRY = "false";
expect(() => captureEvent("session_start", {}, KEY)).not.toThrow();
expect(() => captureEvent("session_start", {}, undefined)).not.toThrow();
expect(() => captureEvent("session_start", {}, KEY, "proj")).not.toThrow();
});
test("every event carries os_version (matches telemetry.py schema)", () => {
const props = buildEvent("session_start", {}, KEY)!
.properties as Record<string, unknown>;
expect(typeof props.os_version).toBe("string");
});
test("project_hash is sha256(projectId) when a project id is supplied", async () => {
const { createHash } = await import("node:crypto");
const expected = createHash("sha256").update("acme-repo").digest("hex");
const props = buildEvent("session_start", {}, KEY, "acme-repo")!
.properties as Record<string, unknown>;
expect(props.project_hash).toBe(expected);
});
test("project_hash is omitted when no project id is supplied (no raw ids leak)", () => {
const props = buildEvent("session_start", {}, KEY)!
.properties as Record<string, unknown>;
expect("project_hash" in props).toBe(false);
});
test("expanded event types all use the shared plugin.* namespace", () => {
for (const ev of ["user_prompt", "bash_error", "pre_compact", "session_stop"]) {
expect(buildEvent(ev, {}, KEY)!.event).toBe(`plugin.${ev}`);
}
});
});