fix(hooks): use is_dir() for palace root check (review feedback)

Both @igorls and the Qodo bot flagged that `_palace_root_exists()` used
`Path.exists()`, which returns True for a regular file. A stray file at
`~/.mempalace` would let the kill-switch be bypassed and crash later in
`STATE_DIR.mkdir()` with NotADirectoryError.

Switched to `Path.is_dir()`. Also fold `_log()`'s inline check through
`_palace_root_exists()` so both kill-switch sites use the same predicate.

New test pins the behavior: a regular file at the palace root path is
treated as absent (hook short-circuits, _log does not crash, the stray
file is left untouched).
This commit is contained in:
lcatlett
2026-05-02 20:37:47 -04:00
parent 8472d553a3
commit 2d50b214d4
2 changed files with 39 additions and 2 deletions
+32
View File
@@ -1035,3 +1035,35 @@ def test_existing_dir_proceeds_normally(tmp_path, monkeypatch):
# _log should have created the state dir under the existing palace root
assert (fake_root / "hook_state").exists()
assert (fake_root / "hook_state" / "hook.log").is_file()
def test_regular_file_at_palace_root_treated_as_absent(tmp_path, monkeypatch):
"""A regular file at ~/.mempalace must be treated the same as absent.
``Path.exists()`` returns True for a regular file, which would let the
kill-switch be bypassed and crash later when ``STATE_DIR.mkdir()`` runs
on ``NotADirectoryError``. ``_palace_root_exists()`` must use
``is_dir()`` so a stray file (or broken symlink) short-circuits cleanly.
"""
fake_root = tmp_path / "file-not-dir"
fake_root.write_text("oops, this is a file not a directory")
monkeypatch.setattr(hooks_cli_mod, "PALACE_ROOT", fake_root)
monkeypatch.setattr(hooks_cli_mod, "STATE_DIR", fake_root / "hook_state")
monkeypatch.setattr(hooks_cli_mod, "_state_dir_initialized", False)
# _palace_root_exists() is the source of truth — it must return False.
assert hooks_cli_mod._palace_root_exists() is False
# Hooks must short-circuit (return {} on stdout) and not touch disk.
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
hook_session_start({"session_id": "file-at-root"}, "claude-code")
assert json.loads(buf.getvalue() or "{}") == {}
# _log must also short-circuit — it must NOT try to mkdir a path under a
# regular file (which would raise NotADirectoryError).
_log("test message") # would raise if not short-circuited
# The stray file is left untouched; we never try to convert it.
assert fake_root.is_file()
assert fake_root.read_text() == "oops, this is a file not a directory"