test(issue-45): the herdr-config tests collect on Python < 3.11; the docs say what that Python loses
tests/test_issue45_herdr_config.py imported tomllib at module level. On Python 3.9 - macOS's /usr/bin/python3, which the venvs and the bootstrap's python3 use here - the import failed during collection and pytest ran no test of the whole suite. The import is optional now, and the 29 tests that parse a result with tomllib or expect the user's settings kept are marked needs_tomllib: on Python < 3.11 herdr_config deliberately writes the key-only config (restore still off, the other settings dropped, a warning on stderr), which test_without_tomllib_restore_is_still_off covers on every Python. Python 3.9: 22 passed / 29 skipped; Python 3.11: 51 passed. create SKILL.md (same line, so the #42 inventory is unchanged) and the 4.8.5 entry state that building the server config needs tomllib (Python 3.11+) in the python3 on PATH, and what an older python3 loses. Checked with Python 3.9 in a throwaway HOME: the warning reaches stderr (the bootstrap captures only stdout) and the user's file is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
35935f9e8a
commit
36f0b8dfd6
3 files changed
+26
-3
No files matched your search
@@ -13,9 +13,13 @@ import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
try: # Python 3.11+; on older Pythons herdr_config writes the key-only config
|
||||
import tomllib
|
||||
except ImportError: # pragma: no cover - depends on the interpreter
|
||||
tomllib = None
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -25,6 +29,12 @@ from lib_py import herdr_config as hc # noqa: E402
|
||||
|
||||
HERDR = shutil.which("herdr")
|
||||
|
||||
# Python < 3.11 has no tomllib: herdr_config then writes the key-only config
|
||||
# (restore still off, the user's other settings dropped, with a warning), so
|
||||
# the tests that parse a result or expect the user's settings kept need it.
|
||||
# test_without_tomllib_restore_is_still_off covers that fallback everywhere.
|
||||
needs_tomllib = pytest.mark.skipif(tomllib is None, reason="needs tomllib (Python 3.11+)")
|
||||
|
||||
# (name, user config text or None, expect the user's other settings kept, expect a warning)
|
||||
CASES = [
|
||||
("missing", None, True, False),
|
||||
@@ -58,6 +68,7 @@ def _without_key(d):
|
||||
return d
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
@pytest.mark.parametrize("name,user,kept,warned", CASES, ids=[c[0] for c in CASES])
|
||||
def test_merge_always_parses_with_restore_off(name, user, kept, warned):
|
||||
text, warnings = hc.server_config_text(user)
|
||||
@@ -70,6 +81,7 @@ def test_merge_always_parses_with_restore_off(name, user, kept, warned):
|
||||
assert text == hc.MINIMAL
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
def test_a_non_bool_restore_value_is_replaced():
|
||||
# herdr would reject the whole file for this; the key is ours to set anyway.
|
||||
text, warnings = hc.server_config_text('[session]\nresume_agents_on_restore = "no"\n')
|
||||
@@ -121,6 +133,7 @@ def test_source_is_herdr_config_path_unless_generated(tmp_path):
|
||||
str(tmp_path / "x" / "herdr" / "config.toml")
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
def test_written_next_to_source_with_its_mode(tmp_path):
|
||||
cfg_dir = tmp_path / "home" / ".config" / "herdr"
|
||||
cfg_dir.mkdir(parents=True)
|
||||
@@ -140,6 +153,7 @@ def test_written_next_to_source_with_its_mode(tmp_path):
|
||||
assert [p.name for p in cfg_dir.iterdir() if p.name.endswith(".tmp")] == []
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
def test_written_without_any_user_config(tmp_path):
|
||||
path, warnings = hc.write_server_config(_env(tmp_path))
|
||||
assert path == str(tmp_path / "home" / ".config" / "herdr" / hc.OUTPUT_NAME) and warnings == []
|
||||
@@ -147,6 +161,7 @@ def test_written_without_any_user_config(tmp_path):
|
||||
assert tomllib.loads(Path(path).read_text()) == {"session": {"resume_agents_on_restore": False}}
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
def test_rewritten_on_every_call(tmp_path):
|
||||
cfg_dir = tmp_path / "home" / ".config" / "herdr"
|
||||
cfg_dir.mkdir(parents=True)
|
||||
@@ -157,6 +172,7 @@ def test_rewritten_on_every_call(tmp_path):
|
||||
assert tomllib.loads(Path(path).read_text())["ui"]["confirm_close"] is False
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
@pytest.mark.skipif(os.geteuid() == 0, reason="root ignores directory permissions")
|
||||
def test_unwritable_config_dir_falls_back_to_mam_dir(tmp_path):
|
||||
cfg_dir = tmp_path / "home" / ".config" / "herdr"
|
||||
@@ -184,6 +200,7 @@ def _tag(source: Path) -> str:
|
||||
return hashlib.sha256(os.path.realpath(source).encode()).hexdigest()[:12]
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
@pytest.mark.skipif(os.geteuid() == 0, reason="root ignores directory permissions")
|
||||
def test_a_foreign_fallback_file_is_kept(tmp_path):
|
||||
cfg_dir = tmp_path / "cfg"
|
||||
@@ -204,6 +221,7 @@ def test_a_foreign_fallback_file_is_kept(tmp_path):
|
||||
assert tomllib.loads(Path(path).read_text())["ui"]["confirm_close"] is False
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
@pytest.mark.parametrize("alias", [False, True], ids=["same-path", "symlink-alias"])
|
||||
def test_the_fallback_is_never_the_source(tmp_path, alias):
|
||||
mam = tmp_path / ".mam"
|
||||
@@ -243,6 +261,7 @@ def test_no_write_when_every_name_is_taken(tmp_path):
|
||||
assert all(f.read_bytes() == b"# foreign\n" for f in taken)
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
def test_cli_prints_the_path(tmp_path):
|
||||
env = dict(os.environ, HOME=str(tmp_path), PYTHONPATH=str(REPO_ROOT / ".agents" / "skills"))
|
||||
env.pop("HERDR_CONFIG_PATH", None)
|
||||
@@ -295,6 +314,7 @@ def _cli(tmp_path, source: Path, *extra) -> subprocess.CompletedProcess:
|
||||
(b"session = 1\n[ui]\nconfirm_close = false\n", False), # non-table session (R1)
|
||||
(b"[ui]\nconfirm_close = false\n# \xff\xfe\n", False), # not UTF-8 (R1)
|
||||
], ids=["valid", "schema-error", "session-scalar", "invalid-utf8"])
|
||||
@needs_tomllib
|
||||
def test_cli_always_hands_herdr_a_restore_off_config_it_accepts(tmp_path, body, kept):
|
||||
source = tmp_path / "custom.toml"
|
||||
source.write_bytes(body)
|
||||
@@ -312,6 +332,7 @@ def test_cli_always_hands_herdr_a_restore_off_config_it_accepts(tmp_path, body,
|
||||
assert source.read_bytes() == body
|
||||
|
||||
|
||||
@needs_tomllib
|
||||
@pytest.mark.skipif(not HERDR, reason="herdr is not installed")
|
||||
def test_a_config_herdr_rejects_is_never_left_in_place(tmp_path):
|
||||
# the merge itself is fine for tomllib; only herdr's schema says no
|
||||
@@ -324,6 +345,7 @@ def test_a_config_herdr_rejects_is_never_left_in_place(tmp_path):
|
||||
|
||||
# --- the user's file is never the output (review R3) -----------------------
|
||||
|
||||
@needs_tomllib
|
||||
def test_source_named_like_the_output_is_not_overwritten(tmp_path):
|
||||
source = tmp_path / hc.OUTPUT_NAME
|
||||
source.write_bytes(b"[ui]\nconfirm_close = false\n")
|
||||
|
||||
Reference in new issue
Block a user