mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(memory): cover the remaining setup-time .env reads with utf-8-sig
Follow-up to review feedback: - mem0 _prompt_api_key read .env with the locale default, so a Notepad BOM hid the first key from the masked current-value lookup; read it with utf-8-sig + errors=replace like the canonical readers in hermes_cli/config.py. - hindsight _load_simple_env used plain utf-8; it also parses the Hermes .env during post_setup, where a BOM stuck to the first key. Switch to utf-8-sig + errors=replace. - Add hindsight regressions: BOM key matching in _load_simple_env and in the cloud post_setup writer, plus non-ASCII round-trip preservation, and a mem0 regression for the BOM'd masked-key lookup. The BOM tests fail without the fix on any platform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
75afc47baa
commit
f1ea4a56c2
4 changed files with 92 additions and 2 deletions
|
|
@ -496,7 +496,9 @@ def _load_simple_env(path) -> dict[str, str]:
|
|||
return {}
|
||||
|
||||
values: dict[str, str] = {}
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
# utf-8-sig, not plain utf-8: this is also used on the Hermes .env during
|
||||
# post_setup, and a Notepad BOM would otherwise stick to the first key.
|
||||
for line in path.read_text(encoding="utf-8-sig", errors="replace").splitlines():
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
|
|
|
|||
|
|
@ -500,7 +500,12 @@ def _prompt_api_key(label: str, env_var: str, hermes_home: str) -> str:
|
|||
if not existing:
|
||||
env_path = Path(hermes_home) / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text().splitlines():
|
||||
# BOM-tolerant read matching the canonical .env readers in
|
||||
# hermes_cli/config.py; a Notepad BOM on the first line would
|
||||
# otherwise defeat the startswith() key match below.
|
||||
for line in env_path.read_text(
|
||||
encoding="utf-8-sig", errors="replace"
|
||||
).splitlines():
|
||||
if line.startswith(f"{env_var}="):
|
||||
existing = line.split("=", 1)[1].strip()
|
||||
break
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from plugins.memory.hindsight import (
|
|||
REFLECT_SCHEMA,
|
||||
RETAIN_SCHEMA,
|
||||
_load_config,
|
||||
_load_simple_env,
|
||||
_build_embedded_profile_env,
|
||||
_normalize_observation_scopes,
|
||||
_normalize_retain_tags,
|
||||
|
|
@ -1819,3 +1820,62 @@ def test_save_config_sets_owner_only_permissions(tmp_path):
|
|||
assert config_file.exists()
|
||||
mode = stat.S_IMODE(config_file.stat().st_mode)
|
||||
assert mode == 0o600, f"Expected 0o600 (owner-only), got {oct(mode)}"
|
||||
|
||||
|
||||
class TestLoadSimpleEnv:
|
||||
def test_bom_first_key_is_recognized(self, tmp_path):
|
||||
"""A Notepad-edited .env carries a BOM; the first key must still parse
|
||||
instead of becoming '\ufeffHINDSIGHT_LLM_API_KEY'."""
|
||||
env_path = tmp_path / ".env"
|
||||
env_path.write_bytes("HINDSIGHT_LLM_API_KEY=sk-test\n".encode("utf-8"))
|
||||
values = _load_simple_env(env_path)
|
||||
assert values.get("HINDSIGHT_LLM_API_KEY") == "sk-test"
|
||||
|
||||
def test_non_ascii_values_read_intact(self, tmp_path):
|
||||
env_path = tmp_path / ".env"
|
||||
env_path.write_bytes("PROXY_NOTE=café-zürich-完了\n".encode("utf-8"))
|
||||
values = _load_simple_env(env_path)
|
||||
assert values["PROXY_NOTE"] == "café-zürich-完了"
|
||||
|
||||
|
||||
class TestPostSetupEnvEncoding:
|
||||
def _run_cloud_post_setup(self, tmp_path, monkeypatch):
|
||||
"""Drive post_setup through the cloud path with piped stdin."""
|
||||
import io
|
||||
import shutil as shutil_mod
|
||||
|
||||
monkeypatch.setattr("hermes_cli.memory_setup._curses_select",
|
||||
lambda *a, **kw: 0) # cloud mode
|
||||
monkeypatch.setattr("hermes_cli.config.save_config", lambda c: None)
|
||||
monkeypatch.setattr(shutil_mod, "which", lambda *_: None) # skip uv install
|
||||
# First line: API key prompt (readline). Second line: API URL (input).
|
||||
monkeypatch.setattr(sys, "stdin", io.StringIO("sk-new\n\n"))
|
||||
|
||||
provider = HindsightMemoryProvider()
|
||||
provider.post_setup(str(tmp_path), {"memory": {}})
|
||||
|
||||
def test_bom_first_key_updated_in_place(self, tmp_path, monkeypatch):
|
||||
"""The setup writer reads the existing .env BOM-tolerantly, so a
|
||||
BOM'd first key is matched and rewritten, not duplicated."""
|
||||
env_path = tmp_path / ".env"
|
||||
env_path.write_bytes("HINDSIGHT_API_KEY=old\n".encode("utf-8"))
|
||||
|
||||
self._run_cloud_post_setup(tmp_path, monkeypatch)
|
||||
|
||||
content = env_path.read_text(encoding="utf-8")
|
||||
assert content.count("HINDSIGHT_API_KEY=") == 1
|
||||
assert "HINDSIGHT_API_KEY=sk-new" in content
|
||||
assert "old" not in content
|
||||
assert "" not in content
|
||||
|
||||
def test_non_ascii_lines_survive_round_trip(self, tmp_path, monkeypatch):
|
||||
"""Unrelated non-ASCII .env content must be copied through as UTF-8
|
||||
(the locale codec would crash or mangle it on Windows)."""
|
||||
env_path = tmp_path / ".env"
|
||||
env_path.write_bytes("PROXY_NOTE=café-zürich-完了\n".encode("utf-8"))
|
||||
|
||||
self._run_cloud_post_setup(tmp_path, monkeypatch)
|
||||
|
||||
content = env_path.read_text(encoding="utf-8")
|
||||
assert "PROXY_NOTE=café-zürich-完了" in content
|
||||
assert "HINDSIGHT_API_KEY=sk-new" in content
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from plugins.memory.mem0._setup import (
|
|||
parse_flags,
|
||||
build_oss_config,
|
||||
_write_env,
|
||||
_prompt_api_key,
|
||||
post_setup,
|
||||
_check_qdrant_path,
|
||||
_check_ollama,
|
||||
|
|
@ -202,6 +203,28 @@ class TestWriteEnv:
|
|||
assert "OPENAI_API_KEY=new" in content
|
||||
|
||||
|
||||
class TestPromptApiKey:
|
||||
|
||||
def test_existing_key_found_behind_bom(self, tmp_path, monkeypatch):
|
||||
"""The masked-current-value lookup must see a key on the BOM'd first
|
||||
line of a Notepad-edited .env instead of prompting from scratch."""
|
||||
env_path = tmp_path / ".env"
|
||||
env_path.write_bytes("OPENAI_API_KEY=sk-existing\n".encode("utf-8"))
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
|
||||
prompts: list[str] = []
|
||||
|
||||
def _fake_getpass(prompt):
|
||||
prompts.append(prompt)
|
||||
return ""
|
||||
|
||||
monkeypatch.setattr("plugins.memory.mem0._setup.getpass.getpass", _fake_getpass)
|
||||
_prompt_api_key("OpenAI", "OPENAI_API_KEY", str(tmp_path))
|
||||
|
||||
assert len(prompts) == 1
|
||||
assert "current: ...ting" in prompts[0]
|
||||
|
||||
|
||||
class TestPostSetup:
|
||||
|
||||
def test_platform_flag_mode(self, tmp_path, monkeypatch):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue