Files
wehub-resource-sync 555e282cc4
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
chore: import upstream snapshot with attribution
2026-07-13 13:03:45 +08:00

262 lines
9.1 KiB
Python

"""Tests for telemetry.py — fire-and-forget PostHog plugin telemetry."""
from __future__ import annotations
import json
import os
import sys
import urllib.error
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
sys.path.insert(0, os.path.abspath(SCRIPTS_DIR))
def test_import_succeeds():
import telemetry
assert hasattr(telemetry, "emit")
assert hasattr(telemetry, "main")
def test_opt_out_skips_send(monkeypatch):
import telemetry
monkeypatch.setenv("MEM0_TELEMETRY", "false")
sent = []
monkeypatch.setattr(telemetry, "send", lambda p: sent.append(p))
telemetry.emit("session_start")
assert sent == []
def test_opt_out_variants(monkeypatch):
import telemetry
for val in ("0", "no", "off", "FALSE", "No"):
monkeypatch.setenv("MEM0_TELEMETRY", val)
assert not telemetry.is_enabled()
def test_enabled_by_default(monkeypatch):
import telemetry
monkeypatch.delenv("MEM0_TELEMETRY", raising=False)
assert telemetry.is_enabled()
def test_posthog_payload_structure(monkeypatch):
import telemetry
monkeypatch.setenv("MEM0_RESOLVED_USER_ID", "testuser")
monkeypatch.setenv("MEM0_PROJECT_ID", "test-project")
monkeypatch.delenv("MEM0_API_KEY", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
payload = telemetry.build_posthog_payload("plugin.session_start", {"memory_count": 5})
assert payload["api_key"] == telemetry.POSTHOG_API_KEY
assert payload["event"] == "plugin.session_start"
assert "distinct_id" in payload
assert payload["properties"]["source"] == "plugin"
assert isinstance(payload["properties"]["plugin_version"], str)
assert payload["properties"]["plugin_version"] != ""
assert payload["properties"]["memory_count"] == 5
assert payload["properties"]["$process_person_profile"] is False
raw = json.dumps(payload)
assert "testuser" not in raw
assert "test-project" not in raw
def test_system_props_override_caller_props(monkeypatch):
"""H8: system properties must win over caller-supplied properties."""
import telemetry
monkeypatch.setenv("MEM0_RESOLVED_USER_ID", "testuser")
monkeypatch.setenv("MEM0_PROJECT_ID", "test-project")
monkeypatch.delenv("MEM0_API_KEY", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
# Caller tries to override system-controlled properties
caller_props = {
"source": "CALLER_OVERRIDE",
"platform": "CALLER_OVERRIDE",
"plugin_version": "CALLER_OVERRIDE",
"memory_count": 42,
}
payload = telemetry.build_posthog_payload("plugin.test", caller_props)
props = payload["properties"]
# System props must win
assert props["source"] == "plugin"
assert props["platform"] == telemetry.detect_platform()
assert props["plugin_version"] == telemetry._load_plugin_version(telemetry.detect_platform())
# Caller-only props still present
assert props["memory_count"] == 42
def test_distinct_id_from_api_key(monkeypatch):
import hashlib
import telemetry
monkeypatch.setenv("MEM0_API_KEY", "m0-testkey123")
expected = hashlib.sha256(b"m0-testkey123").hexdigest()[:32]
assert telemetry._distinct_id() == expected
def test_distinct_id_fallback_no_key(monkeypatch):
import telemetry
monkeypatch.delenv("MEM0_API_KEY", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
monkeypatch.setenv("MEM0_RESOLVED_USER_ID", "kartik")
assert telemetry._distinct_id() == telemetry._sha256("kartik")
def test_hash_deterministic():
import telemetry
h1 = telemetry._sha256("same-value")
h2 = telemetry._sha256("same-value")
assert h1 == h2
assert h1 != telemetry._sha256("different-value")
def test_platform_claude_code(monkeypatch):
import telemetry
monkeypatch.delenv("MEM0_PLATFORM", raising=False)
monkeypatch.delenv("ANTIGRAVITY_PLUGIN_ROOT", raising=False)
monkeypatch.delenv("PLUGIN_ROOT", raising=False)
monkeypatch.delenv("CURSOR_PLUGIN_ROOT", raising=False)
monkeypatch.setenv("CLAUDECODE", "1")
assert telemetry.detect_platform() == "claude-code"
def test_platform_cursor(monkeypatch):
import telemetry
monkeypatch.delenv("MEM0_PLATFORM", raising=False)
monkeypatch.delenv("ANTIGRAVITY_PLUGIN_ROOT", raising=False)
monkeypatch.delenv("PLUGIN_ROOT", raising=False)
monkeypatch.delenv("CLAUDECODE", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_ROOT", raising=False)
monkeypatch.setenv("CURSOR_PLUGIN_ROOT", "/path")
assert telemetry.detect_platform() == "cursor"
def test_platform_codex(monkeypatch):
import telemetry
monkeypatch.delenv("MEM0_PLATFORM", raising=False)
monkeypatch.delenv("ANTIGRAVITY_PLUGIN_ROOT", raising=False)
monkeypatch.delenv("CLAUDECODE", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_ROOT", raising=False)
monkeypatch.delenv("CURSOR_PLUGIN_ROOT", raising=False)
monkeypatch.setenv("PLUGIN_ROOT", "/path")
assert telemetry.detect_platform() == "codex"
def test_platform_explicit_override(monkeypatch):
"""MEM0_PLATFORM wins over auto-detection so each editor can label
itself reliably even when host env vars are ambiguous or absent."""
import telemetry
monkeypatch.setenv("CLAUDE_PLUGIN_ROOT", "/path") # conflicting auto-signal
monkeypatch.setenv("MEM0_PLATFORM", "cursor")
assert telemetry.detect_platform() == "cursor"
def test_platform_antigravity(monkeypatch):
"""Antigravity sets CLAUDE_PLUGIN_ROOT for compatibility but must be
attributed to its own platform, not claude-code."""
import telemetry
monkeypatch.delenv("MEM0_PLATFORM", raising=False)
monkeypatch.setenv("ANTIGRAVITY_PLUGIN_ROOT", "/ext")
monkeypatch.setenv("CLAUDE_PLUGIN_ROOT", "/ext") # antigravity sets both
assert telemetry.detect_platform() == "antigravity"
def test_plugin_version_is_per_editor(monkeypatch):
"""Each editor reports the version from its OWN manifest. Antigravity is on
a 0.1.x line while Claude/Cursor/Codex are on 0.2.x, so they must not all
report the same shared version."""
import telemetry
plugin_dir = os.path.join(os.path.dirname(__file__), "..")
manifests = {
"antigravity": "plugin.json",
"claude-code": os.path.join(".claude-plugin", "plugin.json"),
"cursor": os.path.join(".cursor-plugin", "plugin.json"),
"codex": os.path.join(".codex-plugin", "plugin.json"),
}
for plat, rel in manifests.items():
monkeypatch.setenv("MEM0_PLATFORM", plat)
with open(os.path.join(plugin_dir, rel)) as f:
expected = json.load(f)["version"]
payload = telemetry.build_posthog_payload("plugin.test")
assert payload["properties"]["plugin_version"] == expected, f"{plat} should report {expected} from {rel}"
def test_send_fails_silently(monkeypatch):
import telemetry
def raise_error(req, timeout):
raise urllib.error.URLError("connection refused")
monkeypatch.setattr(telemetry.urllib.request, "urlopen", raise_error)
telemetry.send({"event": "test"})
def test_cli_exits_zero_when_disabled(monkeypatch):
import telemetry
monkeypatch.setenv("MEM0_TELEMETRY", "false")
monkeypatch.setattr(sys, "argv", ["telemetry.py", "session_start"])
assert telemetry.main() == 0
def test_cli_no_args_exits_nonzero(monkeypatch):
import telemetry
monkeypatch.delenv("MEM0_TELEMETRY", raising=False)
monkeypatch.setattr(sys, "argv", ["telemetry.py"])
assert telemetry.main() == 1
def test_cursor_wrappers_pin_platform():
"""Cursor wrappers delegate to the shared scripts, which auto-detect the
platform from host env vars. Since Cursor may not export CURSOR_PLUGIN_ROOT
to the subprocess, each wrapper must pin MEM0_PLATFORM=cursor so the
delegated telemetry is attributed to cursor, not the 'plugin' fallback."""
scripts_dir = os.path.join(os.path.dirname(__file__), "..", "scripts")
cursor_wrappers = [
"on_session_start_cursor.sh",
"on_user_prompt_cursor.sh",
"on_post_tool_use_cursor.sh",
"on_pre_compact_cursor.sh",
"on_stop_cursor.sh",
]
for name in cursor_wrappers:
with open(os.path.join(scripts_dir, name)) as f:
content = f.read()
assert "export MEM0_PLATFORM=cursor" in content, f"{name} must `export MEM0_PLATFORM=cursor` before delegating"
def test_codex_hooks_pin_platform():
"""Codex installs standalone hooks (absolute paths) via install_codex_hooks.py,
so PLUGIN_ROOT is not set at runtime and the platform falls back to 'plugin'.
Codex runs hook commands through a shell, so every command pins
MEM0_PLATFORM=codex inline for correct attribution."""
hooks_path = os.path.join(os.path.dirname(__file__), "..", "hooks", "codex-hooks.json")
with open(hooks_path) as f:
config = json.load(f)
commands = [
h["command"] for entries in config["hooks"].values() for entry in entries for h in entry.get("hooks", [])
]
assert commands, "expected at least one codex hook command"
for cmd in commands:
assert "MEM0_PLATFORM=codex" in cmd, f"codex hook command missing platform pin: {cmd}"