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())