fix(mem0-plugin): v0.2.2 (#5234)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user