tankslab.com/recall/source

tests/test_install_wiring.py

197 lines, exactly as they ship in the download.

#!/usr/bin/env python3 """The installer must not write a hook form that a shell will refuse to run. WHY THIS FILE EXISTS Hooks written as one command string are handed to a shell. On Linux and macOS that is bash. On Windows without Git Bash it is PowerShell, and PowerShell does NOT execute a quoted path at the start of a line - it parses it as a string expression and stops: PS> "C:\\Windows\\System32\\whoami.exe" "arg1" Unexpected token '"arg1"' in expression or statement. That was measured on Windows, not reasoned about. It meant every hook this installer wrote did nothing whatsoever on a Windows machine without Git Bash: no capture, no handoff, no help at compaction, on a platform the README promises. Nothing looks broken from inside the tool - the hooks are present in settings.json and read correctly. The exec form (command + args) takes no shell at all, so there is nothing to parse and spaces in paths stop mattering. On Windows spaces are the normal case: "C:\\Users\\First Last", "Program Files". These checks run on every platform. A Linux developer must not be able to ship a change that breaks Windows without a test going red in front of them. """ from __future__ import annotations import json import os import shutil import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent def isolated(home: Path, store: Path | None = None) -> dict: """An environment where "the user's home directory" is a temporary one. ⚠️ SETTING HOME IS NOT ENOUGH, AND THE DIFFERENCE IS NOT COSMETIC. On Windows, Path.home() reads USERPROFILE first and ignores HOME entirely. A test that sets only HOME therefore runs the installer against the REAL profile - so running this suite would quietly rewrite the tester's own live ~/.claude/settings.json while the assertions read an empty temp directory and reported failures. A test that edits the machine it is checking is worse than a test that fails. Every variable any platform uses to answer "where is home" is redirected here, plus the two application-data roots the store can land in. """ store = store or (home / "store") return dict( os.environ, HOME=str(home), USERPROFILE=str(home), HOMEDRIVE="", HOMEPATH=str(home), LOCALAPPDATA=str(home / "AppData" / "Local"), APPDATA=str(home / "AppData" / "Roaming"), XDG_DATA_HOME=str(home / "xdg"), RECALL_HOME=str(store), FRIDAY_RECALL_ROOT=str(store), ) passed = failed = 0 def check(label, ok, detail=""): global passed, failed if ok: passed += 1 print(f" ok {label}") else: failed += 1 print(f" FAIL {label}") for line in str(detail).splitlines()[:6]: print(f" {line}") def main() -> int: # NOTE the prefix. It deliberately contains no product name: the temp path is # part of the install path, and an earlier version of this test used a prefix # that DID contain it, which quietly propped up the very heuristic the # renamed-folder check exists to condemn. The mutation stayed green and the # test proved nothing. tmp = Path(tempfile.mkdtemp(prefix="inst-wiring-")) home = tmp / "home" (home / ".claude").mkdir(parents=True) # Pre-existing settings that MUST survive, including someone else's hook. (home / ".claude" / "settings.json").write_text(json.dumps({ "model": "opus", "hooks": {"Stop": [{"hooks": [{"type": "command", "command": "echo somebody-elses-hook"}]}]}, }), encoding="utf-8") env = isolated(home, tmp / "store") # First, the refusal that has to happen when this package is being VERIFIED rather # than installed. A person checking the tarball extracts it to a temporary # directory; wiring live hooks to a path that is about to be deleted would leave # every future session capturing nothing, silently. So from a temporary path the # installer must decline to touch settings.json unless told otherwise. # # ⚠️ This check also earns its keep by making the rest of the file honest. Without # it these tests passed from the development tree and FAILED from the extracted # tarball - four red lines in front of anyone verifying the download, caused by a # safety feature working correctly. A test suite that only passes from the author's # own directory is not a test suite anybody else can use. if ROOT.is_relative_to(Path(tempfile.gettempdir())) or "tmp" in {q.lower() for q in ROOT.parts}: rt = subprocess.run([sys.executable, str(ROOT / "install.py")], capture_output=True, text=True, env=env, cwd=str(ROOT)) touched = json.loads((home / ".claude" / "settings.json").read_text(encoding="utf-8")) check("from a temporary path, hooks are NOT wired without --force-hooks", "NOT wired" in rt.stdout and len(touched.get("hooks", {}).get("Stop", [])) == 1, rt.stdout[-400:]) else: print(" -- running from a permanent path, temp-refusal case not exercised here") r = subprocess.run([sys.executable, str(ROOT / "install.py"), "--force-hooks"], capture_output=True, text=True, env=env, cwd=str(ROOT)) check("installer exits 0", r.returncode == 0, (r.stderr or r.stdout)[-600:]) d = json.loads((home / ".claude" / "settings.json").read_text(encoding="utf-8")) hooks = d.get("hooks", {}) check("unrelated settings survived", d.get("model") == "opus", d) check("somebody else's hook survived", any("somebody-elses-hook" in h.get("command", "") for b in hooks.get("Stop", []) for h in b.get("hooks", [])), hooks.get("Stop")) ours = [(ev, h) for ev, blocks in hooks.items() for b in blocks for h in b.get("hooks", []) if "hook.py" in (h.get("command", "") + " ".join(str(a) for a in h.get("args", []) or []))] check("all four events are wired", len(ours) == 4, [e for e, _ in ours]) for ev, h in ours: # THE WINDOWS CHECK. A command string containing a quoted path is a PowerShell # parser error; the exec form has no shell to parse it. check(f"{ev}: shell-less exec form (no quoted path in `command`)", '"' not in h.get("command", "") and isinstance(h.get("args"), list), h) check(f"{ev}: arguments are separate strings, so spaces in paths are safe", all(isinstance(a, str) for a in h.get("args", [])), h) # SessionEnd must block. Its whole job is to finish writing before the session # goes away, and "async" means "runs in the background without blocking". se = [h for ev, h in ours if ev == "SessionEnd"] check("SessionEnd is NOT async - it must finish before the session goes", se and not se[0].get("async"), se) st = [h for ev, h in ours if ev == "Stop"] check("Stop may be async - the session continues, so the process is still there", bool(st), st) # Re-install must REPLACE, not accumulate - including from a renamed folder. r2 = subprocess.run([sys.executable, str(ROOT / "install.py"), "--force-hooks"], capture_output=True, text=True, env=env, cwd=str(ROOT)) check("re-install exits 0", r2.returncode == 0, (r2.stderr or r2.stdout)[-400:]) d2 = json.loads((home / ".claude" / "settings.json").read_text(encoding="utf-8")) ours2 = [1 for blocks in d2.get("hooks", {}).values() for b in blocks for h in b.get("hooks", []) if "hook.py" in (h.get("command", "") + " ".join(str(a) for a in h.get("args", []) or []))] check("re-installing replaces our hooks instead of doubling them", len(ours2) == 4, f"{len(ours2)} entries after two installs") renamed = tmp / "some-other-name" shutil.copytree(ROOT, renamed, ignore=shutil.ignore_patterns( "__pycache__", "*.tar.gz", "*.sha256")) r3 = subprocess.run([sys.executable, str(renamed / "install.py"), "--force-hooks"], capture_output=True, text=True, env=env, cwd=str(renamed)) d3 = json.loads((home / ".claude" / "settings.json").read_text(encoding="utf-8")) ours3 = [1 for blocks in d3.get("hooks", {}).values() for b in blocks for h in b.get("hooks", []) if "hook.py" in (h.get("command", "") + " ".join(str(a) for a in h.get("args", []) or []))] check("installing from a RENAMED folder replaces the originals", len(ours3) == 4, f"{len(ours3)} entries") # And again from that same renamed folder. THIS is the case the old dedupe could # not survive: it recognised our hooks by looking for a word in the install path, # so once the folder no longer contained that word it could not see its own # previous entry and simply appended another. Every re-install added a copy, and # every event fired once more than the time before - silently, because duplicate # hooks look exactly like working hooks. subprocess.run([sys.executable, str(renamed / "install.py"), "--force-hooks"], capture_output=True, text=True, env=env, cwd=str(renamed)) d4 = json.loads((home / ".claude" / "settings.json").read_text(encoding="utf-8")) ours4 = [1 for blocks in d4.get("hooks", {}).values() for b in blocks for h in b.get("hooks", []) if "hook.py" in (h.get("command", "") + " ".join(str(a) for a in h.get("args", []) or []))] check("re-installing TWICE from a renamed folder still does not double", len(ours4) == 4, f"{len(ours4)} entries after two installs from a renamed dir") shutil.rmtree(tmp, ignore_errors=True) print(f"\n ---- {passed} passed, {failed} failed ----") return 1 if failed else 0 if __name__ == "__main__": raise SystemExit(main())