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
440 lines
12 KiB
Python
440 lines
12 KiB
Python
"""Tests for parse_mem0_config.py — mem0.md retention policy parser."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parse_retention — unit tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_parse_retention_valid_section():
|
|
"""parse_retention extracts day-count policies correctly."""
|
|
from parse_mem0_config import parse_retention
|
|
|
|
content = """\
|
|
# Project Config
|
|
|
|
## Retention
|
|
|
|
session_state: 90d
|
|
compact_summary: 60d
|
|
decision: 180d
|
|
"""
|
|
result = parse_retention(content)
|
|
assert result == {
|
|
"session_state": 90,
|
|
"compact_summary": 60,
|
|
"decision": 180,
|
|
}
|
|
|
|
|
|
def test_parse_retention_forever_returns_none():
|
|
"""parse_retention maps 'forever' to None."""
|
|
from parse_mem0_config import parse_retention
|
|
|
|
content = """\
|
|
## Retention
|
|
|
|
user_preference: forever
|
|
anti_pattern: forever
|
|
session_state: 30d
|
|
"""
|
|
result = parse_retention(content)
|
|
assert result["user_preference"] is None
|
|
assert result["anti_pattern"] is None
|
|
assert result["session_state"] == 30
|
|
|
|
|
|
def test_parse_retention_no_section_returns_empty():
|
|
"""parse_retention returns {} when there is no ## Retention heading."""
|
|
from parse_mem0_config import parse_retention
|
|
|
|
content = """\
|
|
# Project Config
|
|
|
|
## Some Other Section
|
|
|
|
key: value
|
|
"""
|
|
result = parse_retention(content)
|
|
assert result == {}
|
|
|
|
|
|
def test_parse_retention_stops_at_next_heading():
|
|
"""parse_retention stops reading at the next ## heading."""
|
|
from parse_mem0_config import parse_retention
|
|
|
|
content = """\
|
|
## Retention
|
|
|
|
session_state: 7d
|
|
|
|
## Other Section
|
|
|
|
other_key: 999d
|
|
"""
|
|
result = parse_retention(content)
|
|
assert "session_state" in result
|
|
assert "other_key" not in result
|
|
|
|
|
|
def test_parse_retention_malformed_lines_skipped():
|
|
"""Malformed lines (no colon, bad day format) are silently ignored."""
|
|
from parse_mem0_config import parse_retention
|
|
|
|
content = """\
|
|
## Retention
|
|
|
|
session_state: 90d
|
|
bad_line_no_colon
|
|
another: badvalue
|
|
decision: 30d
|
|
"""
|
|
result = parse_retention(content)
|
|
assert result == {"session_state": 90, "decision": 30}
|
|
|
|
|
|
def test_parse_retention_comments_ignored():
|
|
"""Inline # comments are stripped before parsing."""
|
|
from parse_mem0_config import parse_retention
|
|
|
|
content = """\
|
|
## Retention
|
|
|
|
session_state: 90d # rolling 90-day window
|
|
user_preference: forever # never prune preferences
|
|
"""
|
|
result = parse_retention(content)
|
|
assert result["session_state"] == 90
|
|
assert result["user_preference"] is None
|
|
|
|
|
|
def test_parse_retention_case_insensitive_heading():
|
|
"""## retention (lowercase) is matched the same as ## Retention."""
|
|
from parse_mem0_config import parse_retention
|
|
|
|
content = """\
|
|
## retention
|
|
|
|
session_state: 14d
|
|
"""
|
|
result = parse_retention(content)
|
|
assert result == {"session_state": 14}
|
|
|
|
|
|
def test_parse_retention_empty_section_returns_empty():
|
|
"""A ## Retention section with no valid lines returns {}."""
|
|
from parse_mem0_config import parse_retention
|
|
|
|
content = """\
|
|
## Retention
|
|
|
|
# only comments here
|
|
|
|
## Next Section
|
|
"""
|
|
result = parse_retention(content)
|
|
assert result == {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# load_retention_policies — integration tests with tmp files
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_load_retention_policies_with_tmp_file(tmp_path):
|
|
"""load_retention_policies reads a real mem0.md from disk."""
|
|
from parse_mem0_config import load_retention_policies
|
|
|
|
mem0_md = tmp_path / "mem0.md"
|
|
mem0_md.write_text(
|
|
"""\
|
|
# My Project
|
|
|
|
## Retention
|
|
|
|
session_state: 90d
|
|
compact_summary: 60d
|
|
decision: forever
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = load_retention_policies(str(tmp_path))
|
|
assert result == {
|
|
"session_state": 90,
|
|
"compact_summary": 60,
|
|
"decision": None,
|
|
}
|
|
|
|
|
|
def test_load_retention_policies_no_mem0_md_returns_empty(tmp_path):
|
|
"""load_retention_policies returns {} when no mem0.md exists."""
|
|
from parse_mem0_config import load_retention_policies
|
|
|
|
result = load_retention_policies(str(tmp_path))
|
|
assert result == {}
|
|
|
|
|
|
def test_load_retention_policies_no_retention_section_returns_empty(tmp_path):
|
|
"""load_retention_policies returns {} when mem0.md has no ## Retention."""
|
|
from parse_mem0_config import load_retention_policies
|
|
|
|
mem0_md = tmp_path / "mem0.md"
|
|
mem0_md.write_text(
|
|
"""\
|
|
# My Project
|
|
|
|
Some general project notes here.
|
|
No retention section.
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = load_retention_policies(str(tmp_path))
|
|
assert result == {}
|
|
|
|
|
|
def test_load_retention_policies_defaults_to_cwd(tmp_path, monkeypatch):
|
|
"""load_retention_policies uses os.getcwd() when cwd is None."""
|
|
from parse_mem0_config import load_retention_policies
|
|
|
|
monkeypatch.chdir(tmp_path)
|
|
mem0_md = tmp_path / "mem0.md"
|
|
mem0_md.write_text("## Retention\nsession_state: 45d\n", encoding="utf-8")
|
|
|
|
result = load_retention_policies() # no cwd arg
|
|
assert result == {"session_state": 45}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI / main() — subprocess test
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_cli_main_prints_json(tmp_path):
|
|
"""CLI: python parse_mem0_config.py <cwd> prints valid JSON."""
|
|
mem0_md = tmp_path / "mem0.md"
|
|
mem0_md.write_text(
|
|
"## Retention\nsession_state: 90d\nuser_preference: forever\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, os.path.join(SCRIPTS_DIR, "parse_mem0_config.py"), str(tmp_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0
|
|
data = json.loads(result.stdout)
|
|
assert data["session_state"] == 90
|
|
assert data["user_preference"] is None
|
|
|
|
|
|
def test_cli_main_no_file_prints_empty_json(tmp_path):
|
|
"""CLI: prints '{}' when no mem0.md exists."""
|
|
result = subprocess.run(
|
|
[sys.executable, os.path.join(SCRIPTS_DIR, "parse_mem0_config.py"), str(tmp_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0
|
|
assert json.loads(result.stdout) == {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parse_section_kv — unit tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_parse_section_kv_basic():
|
|
"""parse_section_kv extracts key-value pairs from a named section."""
|
|
from parse_mem0_config import parse_section_kv
|
|
|
|
content = """\
|
|
## Search
|
|
|
|
default_limit: 10
|
|
boost_recency: true
|
|
"""
|
|
result = parse_section_kv(content, "Search")
|
|
assert result == {"default_limit": "10", "boost_recency": "true"}
|
|
|
|
|
|
def test_parse_section_kv_missing_section():
|
|
"""parse_section_kv returns {} when section doesn't exist."""
|
|
from parse_mem0_config import parse_section_kv
|
|
|
|
result = parse_section_kv("## Other\nfoo: bar\n", "Search")
|
|
assert result == {}
|
|
|
|
|
|
def test_parse_section_kv_stops_at_next_heading():
|
|
"""parse_section_kv stops at the next ## heading."""
|
|
from parse_mem0_config import parse_section_kv
|
|
|
|
content = """\
|
|
## Identity
|
|
|
|
user_id: kartik
|
|
project_id: mem0
|
|
|
|
## Other
|
|
|
|
ignored: yes
|
|
"""
|
|
result = parse_section_kv(content, "Identity")
|
|
assert result == {"user_id": "kartik", "project_id": "mem0"}
|
|
assert "ignored" not in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parse_section_list — unit tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_parse_section_list_basic():
|
|
"""parse_section_list extracts list items from a named section."""
|
|
from parse_mem0_config import parse_section_list
|
|
|
|
content = """\
|
|
## Categories
|
|
|
|
- architecture_decisions
|
|
- bug_fixes
|
|
- coding_conventions
|
|
"""
|
|
result = parse_section_list(content, "Categories")
|
|
assert result == ["architecture_decisions", "bug_fixes", "coding_conventions"]
|
|
|
|
|
|
def test_parse_section_list_bare_lines():
|
|
"""parse_section_list works with bare lines (no bullet prefix)."""
|
|
from parse_mem0_config import parse_section_list
|
|
|
|
content = """\
|
|
## Categories
|
|
|
|
architecture_decisions
|
|
bug_fixes
|
|
"""
|
|
result = parse_section_list(content, "Categories")
|
|
assert result == ["architecture_decisions", "bug_fixes"]
|
|
|
|
|
|
def test_parse_section_list_missing_section():
|
|
"""parse_section_list returns [] when section doesn't exist."""
|
|
from parse_mem0_config import parse_section_list
|
|
|
|
result = parse_section_list("## Other\n- foo\n", "Categories")
|
|
assert result == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# load_full_config — integration tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_load_full_config_all_sections(tmp_path):
|
|
"""load_full_config extracts all sections from mem0.md."""
|
|
from parse_mem0_config import load_full_config
|
|
|
|
mem0_md = tmp_path / "mem0.md"
|
|
mem0_md.write_text(
|
|
"""\
|
|
# My Project
|
|
|
|
## Retention
|
|
|
|
session_state: 90d
|
|
decision: forever
|
|
|
|
## Search
|
|
|
|
default_limit: 20
|
|
boost_recency: true
|
|
|
|
## Categories
|
|
|
|
- architecture_decisions
|
|
- bug_fixes
|
|
- security_constraints
|
|
|
|
## Identity
|
|
|
|
user_id: kartik
|
|
project_id: my-project
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
config = load_full_config(str(tmp_path))
|
|
assert config["retention"] == {"session_state": 90, "decision": None}
|
|
assert config["search"] == {"default_limit": "20", "boost_recency": "true"}
|
|
assert config["categories"] == ["architecture_decisions", "bug_fixes", "security_constraints"]
|
|
assert config["identity"] == {"user_id": "kartik", "project_id": "my-project"}
|
|
|
|
|
|
def test_load_full_config_partial_sections(tmp_path):
|
|
"""load_full_config only includes sections that exist."""
|
|
from parse_mem0_config import load_full_config
|
|
|
|
mem0_md = tmp_path / "mem0.md"
|
|
mem0_md.write_text("## Retention\nsession_state: 30d\n", encoding="utf-8")
|
|
|
|
config = load_full_config(str(tmp_path))
|
|
assert "retention" in config
|
|
assert "search" not in config
|
|
assert "categories" not in config
|
|
assert "identity" not in config
|
|
|
|
|
|
def test_load_full_config_no_file(tmp_path):
|
|
"""load_full_config returns {} when no mem0.md exists."""
|
|
from parse_mem0_config import load_full_config
|
|
|
|
config = load_full_config(str(tmp_path))
|
|
assert config == {}
|
|
|
|
|
|
def test_cli_full_flag(tmp_path):
|
|
"""CLI: --full prints all sections as JSON."""
|
|
mem0_md = tmp_path / "mem0.md"
|
|
mem0_md.write_text(
|
|
"## Retention\nsession_state: 90d\n\n## Search\nlimit: 10\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, os.path.join(SCRIPTS_DIR, "parse_mem0_config.py"), "--full", str(tmp_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0
|
|
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"
|