fix(mem0-plugin): v0.2.2 (#5234)

This commit is contained in:
Kartik
2026-05-22 21:47:14 +05:30
committed by GitHub
parent 53a3998873
commit 16a7702d09
41 changed files with 889 additions and 675 deletions
+11
View File
@@ -22,6 +22,17 @@ def _scripts_on_path():
sys.path.remove(abs_scripts)
@pytest.fixture(autouse=True)
def _clean_project_map():
"""Remove project_map.json before each test to prevent cross-test pollution."""
map_path = os.path.expanduser("~/.mem0/project_map.json")
if os.path.isfile(map_path):
os.remove(map_path)
yield
if os.path.isfile(map_path):
os.remove(map_path)
@pytest.fixture()
def tmp_git_repo(tmp_path):
"""Create a temp dir with a git repo and HTTPS remote."""
@@ -17,7 +17,7 @@ SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
def test_split_by_headers_cursorrules():
"""split_by_headers correctly splits .cursorrules content on ## headers."""
from import_competing_tools import split_by_headers
from _chunking import split_by_headers
content = """\
# My Cursor Rules
@@ -57,7 +57,7 @@ Write tests for all utility functions.
def test_split_by_headers_copilot():
"""split_by_headers correctly splits copilot-instructions.md on ## headers."""
from import_competing_tools import split_by_headers
from _chunking import split_by_headers
content = """\
## Code Style
@@ -78,7 +78,7 @@ Follow clean architecture principles. Keep business logic in domain layer.
def test_split_by_headers_no_headers():
"""split_by_headers returns entire content as one chunk if no headers found."""
from import_competing_tools import split_by_headers
from _chunking import split_by_headers
content = "This file has no headers at all. Just plain text."
chunks = split_by_headers(content, "## ")
@@ -88,7 +88,7 @@ def test_split_by_headers_no_headers():
def test_split_cline_multiple_md_files(tmp_path):
"""cmd_cline processes multiple .md files from memory-bank/ directory."""
from import_competing_tools import filter_and_truncate
from _chunking import filter_and_truncate
# Create a temporary memory-bank directory with .md files
mb_dir = tmp_path / "memory-bank"
@@ -124,7 +124,7 @@ def test_split_cline_multiple_md_files(tmp_path):
def test_split_by_hr_or_headers_continue():
"""split_by_hr_or_headers correctly splits .continue/rules.md."""
from import_competing_tools import split_by_hr_or_headers
from _chunking import split_by_hr_or_headers
content = """\
## First Section
@@ -150,7 +150,7 @@ Third section without a header (just after HR).
def test_filter_and_truncate_skips_short():
"""filter_and_truncate skips chunks shorter than MIN_CHUNK_CHARS (50)."""
from import_competing_tools import filter_and_truncate
from _chunking import filter_and_truncate
chunks = [
"Short", # < 50 chars, should be filtered
@@ -165,7 +165,7 @@ def test_filter_and_truncate_skips_short():
def test_filter_and_truncate_truncates_long():
"""filter_and_truncate truncates chunks over MAX_CHUNK_CHARS (10000)."""
from import_competing_tools import MAX_CHUNK_CHARS, filter_and_truncate
from _chunking import MAX_CHUNK_CHARS, filter_and_truncate
long_chunk = "X" * (MAX_CHUNK_CHARS + 500)
result = filter_and_truncate([long_chunk])
@@ -422,3 +422,18 @@ def test_cli_full_flag(tmp_path):
data = json.loads(result.stdout)
assert "retention" in data
assert "search" in data
def test_load_full_config_settings_section(tmp_path):
"""Settings section is parsed by load_full_config."""
mem0_md = tmp_path / "mem0.md"
mem0_md.write_text("""\
## Settings
commit_prompts: true
subagent_skip: Explore, Plan, code-reviewer
""")
sys.path.insert(0, SCRIPTS_DIR)
from parse_mem0_config import load_full_config
config = load_full_config(str(tmp_path))
assert config.get("settings", {}).get("commit_prompts") == "true"
assert config.get("settings", {}).get("subagent_skip") == "Explore, Plan, code-reviewer"
-54
View File
@@ -1,54 +0,0 @@
"""Tests for on_pre_commit.py — pre-commit memory capture."""
from __future__ import annotations
import os
from unittest.mock import MagicMock
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
def test_import_succeeds():
"""on_pre_commit module can be imported."""
import on_pre_commit
assert hasattr(on_pre_commit, "main")
def test_no_api_key_exits_zero(monkeypatch):
"""main() exits 0 when no API key is set."""
import on_pre_commit
monkeypatch.delenv("MEM0_API_KEY", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
monkeypatch.setattr("sys.stdin", MagicMock(isatty=lambda: True))
assert on_pre_commit.main() == 0
def test_empty_diff_exits_zero(monkeypatch):
"""main() exits 0 when stdin diff is empty."""
from io import StringIO
import on_pre_commit
monkeypatch.setenv("MEM0_API_KEY", "test-key")
monkeypatch.setattr("sys.stdin", StringIO(""))
assert on_pre_commit.main() == 0
def test_get_staged_summary_runs():
"""get_staged_summary doesn't crash even outside a git repo."""
import on_pre_commit
result = on_pre_commit.get_staged_summary()
assert isinstance(result, str)
def test_get_commit_message_runs():
"""get_commit_message doesn't crash even outside a git repo."""
import on_pre_commit
result = on_pre_commit.get_commit_message()
assert isinstance(result, str)
+64
View File
@@ -105,3 +105,67 @@ def test_resolve_project_id_priority_order(tmp_git_repo, monkeypatch):
# Env var overrides everything
monkeypatch.setenv("MEM0_PROJECT_ID", "from-env")
assert resolve_project_id(str(tmp_git_repo)) == "from-env"
def test_remote_hash_key_format(tmp_git_repo):
"""_remote_hash_key() returns 'remote:<16-char-hex>' for a repo with a remote."""
import re
from _project import _remote_hash_key
key = _remote_hash_key(str(tmp_git_repo))
assert re.fullmatch(r"remote:[0-9a-f]{16}", key), (
f"Expected 'remote:<16-char-hex>', got {key!r}"
)
def test_remote_hash_key_no_git(tmp_no_git):
"""_remote_hash_key() returns empty string when not in a git repo."""
from _project import _remote_hash_key
key = _remote_hash_key(str(tmp_no_git))
assert key == ""
def test_save_project_mapping_writes_remote_key(tmp_git_repo):
"""save_project_mapping() writes both the CWD key and the remote hash key."""
import re
from _project import save_project_mapping
save_project_mapping(str(tmp_git_repo), "my-project")
map_path = os.path.expanduser("~/.mem0/project_map.json")
with open(map_path) as f:
data = json.load(f)
assert data[str(tmp_git_repo)] == "my-project"
remote_keys = [k for k in data if re.fullmatch(r"remote:[0-9a-f]{16}", k)]
assert remote_keys, "Expected at least one remote:<hash> key in project_map.json"
assert data[remote_keys[0]] == "my-project"
def test_resolve_project_id_remote_hash_fallback(tmp_git_repo, tmp_path):
"""Moving the project folder: remote hash key is used as fallback."""
from _project import resolve_project_id, save_project_mapping
# Save mapping for original location
save_project_mapping(str(tmp_git_repo), "stable-project")
# Simulate folder move: resolve using a different CWD path that shares the same remote.
# We use a second tmp_git_repo with the same remote URL to mimic a renamed directory.
import subprocess as _sp
new_repo = tmp_path / "moved_repo"
new_repo.mkdir()
_sp.run(["git", "init"], cwd=new_repo, capture_output=True, check=True)
_sp.run(
["git", "remote", "add", "origin", "https://github.com/mem0ai/mem0.git"],
cwd=new_repo,
capture_output=True,
check=True,
)
# The new CWD is NOT in project_map, but remote hash should match
pid = resolve_project_id(str(new_repo))
assert pid == "stable-project", (
f"Expected 'stable-project' via remote hash fallback, got {pid!r}"
)
+30 -2
View File
@@ -57,7 +57,8 @@ def test_posthog_payload_structure(monkeypatch):
assert payload["event"] == "plugin.session_start"
assert "distinct_id" in payload
assert payload["properties"]["source"] == "plugin"
assert payload["properties"]["plugin_version"] == "0.2.1"
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
@@ -66,13 +67,40 @@ def test_posthog_payload_structure(monkeypatch):
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.PLUGIN_VERSION
# 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.md5(b"m0-testkey123").hexdigest()
expected = hashlib.sha256(b"m0-testkey123").hexdigest()[:32]
assert telemetry._distinct_id() == expected