555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
208 lines
7.3 KiB
Python
208 lines
7.3 KiB
Python
"""Tests for write-path app_id migration and API key resolution.
|
|
|
|
Verifies that all scripts writing to the Mem0 API:
|
|
1. Pass app_id as a top-level parameter (not in metadata)
|
|
2. Do NOT include project_id in metadata
|
|
3. Include branch in metadata when available
|
|
4. Use resolve_api_key() for key resolution with userConfig fallback
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
def test_auto_import_post_memory_uses_app_id():
|
|
"""auto_import.post_memory sends app_id top-level, not metadata.project_id."""
|
|
from auto_import import post_memory
|
|
|
|
captured = {}
|
|
|
|
def mock_urlopen(req, timeout=None):
|
|
body = json.loads(req.data.decode("utf-8"))
|
|
captured.update(body)
|
|
resp = MagicMock()
|
|
resp.status = 200
|
|
resp.__enter__ = lambda s: s
|
|
resp.__exit__ = MagicMock(return_value=False)
|
|
return resp
|
|
|
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
|
result = post_memory(
|
|
api_key="test-key",
|
|
content="test content",
|
|
user_id="testuser",
|
|
filename="CLAUDE.md",
|
|
project_id="my-project",
|
|
branch="main",
|
|
)
|
|
|
|
assert result is True
|
|
assert captured["app_id"] == "my-project"
|
|
assert captured["user_id"] == "testuser"
|
|
assert "project_id" not in captured.get("metadata", {})
|
|
assert captured["metadata"]["type"] == "project_profile"
|
|
assert captured["metadata"]["branch"] == "main"
|
|
assert captured["infer"] is False
|
|
|
|
|
|
def test_auto_import_post_memory_omits_empty_branch():
|
|
"""auto_import.post_memory skips branch in metadata when empty."""
|
|
from auto_import import post_memory
|
|
|
|
captured = {}
|
|
|
|
def mock_urlopen(req, timeout=None):
|
|
body = json.loads(req.data.decode("utf-8"))
|
|
captured.update(body)
|
|
resp = MagicMock()
|
|
resp.status = 200
|
|
resp.__enter__ = lambda s: s
|
|
resp.__exit__ = MagicMock(return_value=False)
|
|
return resp
|
|
|
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
|
post_memory("key", "content", "user", "FILE.md", "proj", branch="")
|
|
|
|
assert "branch" not in captured.get("metadata", {})
|
|
|
|
|
|
def test_on_pre_compact_store_memory_uses_app_id():
|
|
"""on_pre_compact.store_memory sends app_id top-level."""
|
|
from on_pre_compact import store_memory
|
|
|
|
captured = {}
|
|
|
|
def mock_urlopen(req, timeout=None):
|
|
body = json.loads(req.data.decode("utf-8"))
|
|
captured.update(body)
|
|
resp = MagicMock()
|
|
resp.status = 200
|
|
resp.__enter__ = lambda s: s
|
|
resp.__exit__ = MagicMock(return_value=False)
|
|
return resp
|
|
|
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
|
result = store_memory(
|
|
api_key="test-key",
|
|
content="session state content",
|
|
user_id="testuser",
|
|
source="pre-compaction",
|
|
session_id="sess-123",
|
|
project_id="my-project",
|
|
branch="feat/auth",
|
|
)
|
|
|
|
assert result is True
|
|
assert captured["app_id"] == "my-project"
|
|
assert captured["user_id"] == "testuser"
|
|
assert "project_id" not in captured.get("metadata", {})
|
|
assert captured["metadata"]["type"] == "session_state"
|
|
assert captured["metadata"]["source"] == "pre-compaction"
|
|
assert captured["metadata"]["branch"] == "feat/auth"
|
|
assert "expiration_date" in captured
|
|
|
|
|
|
def test_capture_compact_summary_store_uses_app_id():
|
|
"""capture_compact_summary.store_summary sends app_id top-level."""
|
|
from capture_compact_summary import store_summary
|
|
|
|
captured = {}
|
|
|
|
def mock_urlopen(req, timeout=None):
|
|
body = json.loads(req.data.decode("utf-8"))
|
|
captured.update(body)
|
|
resp = MagicMock()
|
|
resp.status = 200
|
|
resp.__enter__ = lambda s: s
|
|
resp.__exit__ = MagicMock(return_value=False)
|
|
return resp
|
|
|
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
|
result = store_summary(
|
|
api_key="test-key",
|
|
summary="compact summary text",
|
|
user_id="testuser",
|
|
session_id="sess-456",
|
|
project_id="my-project",
|
|
branch="main",
|
|
)
|
|
|
|
assert result is True
|
|
assert captured["app_id"] == "my-project"
|
|
assert captured["user_id"] == "testuser"
|
|
assert "project_id" not in captured.get("metadata", {})
|
|
assert captured["metadata"]["type"] == "compact_summary"
|
|
assert captured["metadata"]["branch"] == "main"
|
|
assert captured["infer"] is True
|
|
assert "expiration_date" in captured
|
|
|
|
|
|
def test_no_metadata_project_id_anywhere():
|
|
"""Ensure none of the write functions put project_id in metadata."""
|
|
from auto_import import post_memory
|
|
from capture_compact_summary import store_summary
|
|
from on_pre_compact import store_memory
|
|
|
|
bodies = []
|
|
|
|
def mock_urlopen(req, timeout=None):
|
|
body = json.loads(req.data.decode("utf-8"))
|
|
bodies.append(body)
|
|
resp = MagicMock()
|
|
resp.status = 200
|
|
resp.__enter__ = lambda s: s
|
|
resp.__exit__ = MagicMock(return_value=False)
|
|
return resp
|
|
|
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
|
post_memory("k", "c", "u", "f", "proj", "br")
|
|
store_memory("k", "c", "u", "src", "sid", "proj", "br")
|
|
store_summary("k", "s", "u", "sid", "proj", "br")
|
|
|
|
for i, body in enumerate(bodies):
|
|
metadata = body.get("metadata", {})
|
|
assert "project_id" not in metadata, f"Write function #{i} still has metadata.project_id"
|
|
assert body.get("app_id") == "proj", f"Write function #{i} missing app_id top-level"
|
|
|
|
|
|
def test_resolve_api_key_prefers_env_var(monkeypatch):
|
|
"""resolve_api_key returns MEM0_API_KEY when both are set."""
|
|
from _identity import resolve_api_key
|
|
|
|
monkeypatch.setenv("MEM0_API_KEY", "direct-key")
|
|
monkeypatch.setenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", "fallback-key")
|
|
assert resolve_api_key() == "direct-key"
|
|
|
|
|
|
def test_resolve_api_key_falls_back_to_plugin_option(monkeypatch):
|
|
"""resolve_api_key falls back to CLAUDE_PLUGIN_OPTION_MEM0_API_KEY."""
|
|
from _identity import resolve_api_key
|
|
|
|
monkeypatch.delenv("MEM0_API_KEY", raising=False)
|
|
monkeypatch.setenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", "fallback-key")
|
|
assert resolve_api_key() == "fallback-key"
|
|
|
|
|
|
def test_resolve_api_key_returns_empty_when_neither_set(monkeypatch):
|
|
"""resolve_api_key returns empty string when no key is available."""
|
|
from _identity import resolve_api_key
|
|
|
|
monkeypatch.delenv("MEM0_API_KEY", raising=False)
|
|
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_API_KEY", raising=False)
|
|
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
|
|
monkeypatch.setattr("_identity._extract_key_from_shell_profiles", lambda: "")
|
|
assert resolve_api_key() == ""
|
|
|
|
|
|
def test_resolve_api_key_falls_back_to_shell_profile(monkeypatch):
|
|
"""resolve_api_key extracts key from shell profile when env vars are empty."""
|
|
from _identity import resolve_api_key
|
|
|
|
monkeypatch.delenv("MEM0_API_KEY", raising=False)
|
|
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_API_KEY", raising=False)
|
|
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
|
|
monkeypatch.setattr("_identity._extract_key_from_shell_profiles", lambda: "m0-from-profile")
|
|
assert resolve_api_key() == "m0-from-profile"
|