fix(issue-45): review round 2 - an unrunnable config check is not a pass, odd pids never close a pane, nan does not drop settings

codex round 2 (PR #48).

- R1: when `herdr config check` could not run (timeout, OSError), the
  merged config was handed to the server as checked. A merge with a schema
  error makes herdr drop the whole file, restore back on - the #45 failure.
  The merged body is no longer used unchecked: _write_checked falls to the
  fixed MINIMAL config with a warning. When the check cannot run on MINIMAL
  either, MINIMAL is still used (with its own warning) rather than no
  config: returning none would leave restore on, and MINIMAL is a fixed
  two-line file the real-herdr test accepts. Unknown non-zero exits stay as
  they were - herdr exits non-zero for harmless warnings (an unknown
  section) that do not drop the file.
- R2: _shell_in_foreground compared shell_pid and the foreground group with
  ==, so 0/0, False/False, ""/"" or -1/-1 closed the pane. Both must now be
  positive ints (bool excluded) before they are compared.
- R3: the re-parse check used ==, and nan != nan, so a valid TOML nan threw
  away every other user setting even on Python 3.11. _same_doc compares
  documents recursively with nan equal to nan.

Tests: R1 (timeout, OSError) and R3 (top-level, array, nested nan) in
test_issue45_herdr_config.py; R2 in the new tests/test_issue45_restore_unit.py
(a herdr stub: zero, false, empty, negative, null, string, true, float;
missing info; a real shell closed; a busy pane kept). On the previous code
12 of them fail. Python 3.11: 67 passed; Python 3.9: 33 passed / 34 skipped
(tomllib-only). Real-herdr restore tests and related files on 3.9: 197
passed / 1 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-05 17:50:15 +09:00
1 parent e8d9151226
commit ab93776d6d
4 files changed
+125 -4

No files matched your search

+23 -2
View File
@@ -137,6 +137,18 @@ def dumps(doc: dict) -> str:
return "\n".join(lines) + "\n"
def _same_doc(a, b) -> bool:
"""Equality of two parsed TOML documents in which nan equals nan (a valid
TOML value that a plain == never matches, review R3)."""
if isinstance(a, float) and isinstance(b, float) and math.isnan(a) and math.isnan(b):
return True
if isinstance(a, dict) and isinstance(b, dict):
return a.keys() == b.keys() and all(_same_doc(a[k], b[k]) for k in a)
if isinstance(a, list) and isinstance(b, list):
return len(a) == len(b) and all(_same_doc(x, y) for x, y in zip(a, b))
return type(a) is type(b) and a == b
def server_config_text(user_text: Optional[str]) -> Tuple[str, List[str]]:
"""(the TOML body for the server, warnings). The body always parses, has
session.resume_agents_on_restore = false and otherwise equals the source -
@@ -156,7 +168,7 @@ def server_config_text(user_text: Optional[str]) -> Tuple[str, List[str]]:
expected = dict(user, session=dict(session, **{KEY: False}))
try:
text = dumps(expected)
ok = tomllib.loads(text) == expected
ok = _same_doc(tomllib.loads(text), expected)
except (TypeError, tomllib.TOMLDecodeError):
ok = False
if not ok:
@@ -233,7 +245,16 @@ def _write_checked(target: str, source: str, body: str, mode: int, herdr: Option
try:
why = herdr_rejects(target, herdr)
except (OSError, subprocess.SubprocessError) as e:
warnings.append(f"cannot run `{herdr} config check` ({e}); the config is unchecked")
# Not a pass (review R1): an unchecked merge with a schema error
# makes herdr drop the whole file - restore back on. The merged
# body is not used unchecked; MINIMAL is a fixed file herdr 0.9.1
# accepts (test_real_herdr_accepts_every_result), and returning no
# config would leave restore on, so it is used with a warning.
if candidate != MINIMAL:
warnings.append(f"cannot run `{herdr} config check` ({e}); not using an unchecked merge; "
f"{LOSES_SETTINGS}")
continue
warnings.append(f"cannot run `{herdr} config check` ({e}); using the fixed minimal config unchecked")
return True
if why is None:
return True