feat(cli): Platform option parity across Python and Node CLIs (MEM-5893) (#6696)
This commit is contained in:
@@ -27,24 +27,45 @@ def load_settings() -> dict:
|
||||
try:
|
||||
with open(SETTINGS_PATH) as f:
|
||||
user = json.load(f)
|
||||
settings.update({k: v for k, v in user.items() if k in DEFAULTS})
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return settings
|
||||
if isinstance(user, dict):
|
||||
settings.update({k: v for k, v in user.items() if k in DEFAULTS})
|
||||
return settings
|
||||
|
||||
|
||||
def create_default_settings() -> None:
|
||||
def create_default_settings() -> bool:
|
||||
"""Write the default settings file if absent. Returns True if it was created."""
|
||||
SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
if SETTINGS_PATH.exists():
|
||||
return False
|
||||
with open(SETTINGS_PATH, "w") as f:
|
||||
json.dump(DEFAULTS, f, indent=2)
|
||||
f.write("\n")
|
||||
return True
|
||||
|
||||
|
||||
def unknown_keys() -> list[str]:
|
||||
"""Keys present in the user's settings file that no code reads."""
|
||||
if not SETTINGS_PATH.exists():
|
||||
with open(SETTINGS_PATH, "w") as f:
|
||||
json.dump(DEFAULTS, f, indent=2)
|
||||
f.write("\n")
|
||||
return []
|
||||
try:
|
||||
with open(SETTINGS_PATH) as f:
|
||||
user = json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return []
|
||||
if not isinstance(user, dict):
|
||||
return []
|
||||
return sorted(k for k in user if k not in DEFAULTS)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "init":
|
||||
create_default_settings()
|
||||
print(f"Created {SETTINGS_PATH}")
|
||||
if create_default_settings():
|
||||
print(f"Created {SETTINGS_PATH}")
|
||||
ignored = unknown_keys()
|
||||
if ignored:
|
||||
print(f"Ignoring unrecognized settings in {SETTINGS_PATH}: {', '.join(ignored)}")
|
||||
else:
|
||||
print(json.dumps(load_settings()))
|
||||
|
||||
@@ -23,15 +23,14 @@ def _scripts_on_path():
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_project_map(monkeypatch):
|
||||
"""Remove project_map.json and clear MEM0_PROJECT_ID before each test."""
|
||||
def _isolated_home(tmp_path, monkeypatch):
|
||||
"""Point HOME at a tmp dir so ~/.mem0 writes never touch the real home."""
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HOME", str(home))
|
||||
monkeypatch.setenv("USERPROFILE", str(home))
|
||||
monkeypatch.delenv("MEM0_PROJECT_ID", raising=False)
|
||||
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)
|
||||
yield home
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests for scripts/load_settings.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def settings(tmp_path, monkeypatch):
|
||||
import load_settings
|
||||
|
||||
path = tmp_path / ".mem0" / "settings.json"
|
||||
monkeypatch.setattr(load_settings, "SETTINGS_PATH", path)
|
||||
return load_settings
|
||||
|
||||
|
||||
def test_creates_file_when_absent(settings):
|
||||
assert settings.create_default_settings() is True
|
||||
assert settings.SETTINGS_PATH.exists()
|
||||
assert json.loads(settings.SETTINGS_PATH.read_text()) == settings.DEFAULTS
|
||||
|
||||
|
||||
def test_does_not_recreate_or_overwrite_existing(settings):
|
||||
settings.SETTINGS_PATH.parent.mkdir(parents=True)
|
||||
settings.SETTINGS_PATH.write_text('{"search_limit": 42}')
|
||||
|
||||
assert settings.create_default_settings() is False
|
||||
assert json.loads(settings.SETTINGS_PATH.read_text()) == {"search_limit": 42}
|
||||
|
||||
|
||||
def test_user_values_override_defaults(settings):
|
||||
settings.SETTINGS_PATH.parent.mkdir(parents=True)
|
||||
settings.SETTINGS_PATH.write_text('{"search_limit": 3, "auto_save": false}')
|
||||
|
||||
loaded = settings.load_settings()
|
||||
assert loaded["search_limit"] == 3
|
||||
assert loaded["auto_save"] is False
|
||||
assert loaded["global_search"] == settings.DEFAULTS["global_search"]
|
||||
|
||||
|
||||
def test_unknown_keys_are_dropped_but_reported(settings):
|
||||
settings.SETTINGS_PATH.parent.mkdir(parents=True)
|
||||
settings.SETTINGS_PATH.write_text('{"skip_tools": ["Read"], "output_style": "compact"}')
|
||||
|
||||
assert "skip_tools" not in settings.load_settings()
|
||||
assert settings.unknown_keys() == ["output_style", "skip_tools"]
|
||||
|
||||
|
||||
def test_unknown_keys_empty_for_clean_file(settings):
|
||||
settings.create_default_settings()
|
||||
assert settings.unknown_keys() == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("body", ["{not json", '["a", "list"]'])
|
||||
def test_malformed_file_falls_back_to_defaults(settings, body):
|
||||
settings.SETTINGS_PATH.parent.mkdir(parents=True)
|
||||
settings.SETTINGS_PATH.write_text(body)
|
||||
|
||||
assert settings.load_settings() == settings.DEFAULTS
|
||||
assert settings.unknown_keys() == []
|
||||
|
||||
|
||||
def test_init_announces_creation_only_once(_isolated_home):
|
||||
import load_settings
|
||||
|
||||
home = _isolated_home
|
||||
|
||||
def run_init():
|
||||
return subprocess.run(
|
||||
[sys.executable, load_settings.__file__, "init"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
env={**os.environ, "HOME": str(home)},
|
||||
).stdout
|
||||
|
||||
first = run_init()
|
||||
second = run_init()
|
||||
|
||||
assert "Created" in first
|
||||
assert "Created" not in second
|
||||
Reference in New Issue
Block a user