From a54d21033eaaf1562c8089a6e1d72d9082218bdd Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sat, 11 Jul 2026 00:30:35 -0700 Subject: [PATCH] #688 follow-up: probe hygiene SOP + test hygiene. @aipass added docs/probe_hygiene.md (principle: temp=used+deleted+gone, no permanent pointer at a temp path, all 4 defenses + correct probe workflow). Test location-independence: TestRunInit gets a _isolate_cwd autouse fixture (monkeypatch.chdir) so it passes from ANY cwd incl an agent branch dir; 5 test_bootstrap env.AIPASS_HOME tests patch is_throwaway_path->False so they assert correctly even when the repo itself lives under a temp path. Devpulse caught+fixed the 2 update_project tests @aipass missed (same monkeypatch pattern). Verified: 657/657 green in REAL tree AND a fresh /tmp clean-room extraction (was 2 failing in clean-room before the last 2 fixes). --all --- src/aipass/aipass/docs/probe_hygiene.md | 42 +++++++++++++++++++++++ src/aipass/aipass/tests/test_bootstrap.py | 30 +++++++++++++--- src/aipass/aipass/tests/test_init_flow.py | 5 +++ 3 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 src/aipass/aipass/docs/probe_hygiene.md diff --git a/src/aipass/aipass/docs/probe_hygiene.md b/src/aipass/aipass/docs/probe_hygiene.md new file mode 100644 index 00000000..8223cd7a --- /dev/null +++ b/src/aipass/aipass/docs/probe_hygiene.md @@ -0,0 +1,42 @@ +# Probe Hygiene SOP + +Standard operating procedure for throwaway test installs of AIPass. + +## Principle + +Temporary environments are used, then deleted, gone. Nothing permanent may ever point at a temp path. + +## Rules + +- Install probes and throwaway test installs live ONLY in throwaway directories (system temp dir, `/tmp`, Claude Code scratchpad dirs). +- Used = deleted = GONE. Delete the probe directory immediately after the test completes. +- NOTHING permanent may ever point at a temporary path: no global settings (`~/.claude/settings.json` `env.AIPASS_HOME`), no symlinks, no registry entries. +- `aipass install` now refuses throwaway homes automatically. The `--force-global-home` flag is the explicit unsafe override, for probe use only. +- `aipass doctor` now detects a hijacked global `AIPASS_HOME` (nonexistent or temp path) and flags it as an error with fix guidance. + +## What the defenses do + +1. **`is_throwaway_path()`** (bootstrap.py) — detects paths under `tempfile.gettempdir()`, `/tmp` (POSIX), or containing `scratchpad`. Shared gate used by both install and bootstrap. +2. **`run_install()` gate** (install.py) — refuses to proceed when the resolved home is throwaway. Prints a loud `REFUSED` message with guidance. `--force-global-home` overrides. +3. **`_claude_settings()` gate** (bootstrap.py) — refuses to write `env.AIPASS_HOME` into project settings when the detected home is throwaway. Defense-in-depth behind the install gate. +4. **`_check_global_aipass_home()`** (doctor.py) — reads `~/.claude/settings.json` and flags `env.AIPASS_HOME` pointing at a nonexistent or throwaway path as an error. + +## Correct probe workflow + +```bash +# 1. Create throwaway dir +cd /tmp && mkdir aipass_probe && cd aipass_probe + +# 2. Run the probe (install will refuse — this is correct) +aipass install --here +# → REFUSED: '/tmp/aipass_probe' is a temporary/scratchpad path. + +# 3. If you genuinely need a temp install (testing only): +aipass install --here --force-global-home + +# 4. IMMEDIATELY after testing, delete the probe +rm -rf /tmp/aipass_probe + +# 5. Verify global settings are clean +aipass doctor +``` diff --git a/src/aipass/aipass/tests/test_bootstrap.py b/src/aipass/aipass/tests/test_bootstrap.py index 9efc5556..8d793c66 100644 --- a/src/aipass/aipass/tests/test_bootstrap.py +++ b/src/aipass/aipass/tests/test_bootstrap.py @@ -278,8 +278,12 @@ def test_init_project_claude_settings_content(tmp_path): assert "deny" in data["permissions"] -def test_init_project_settings_no_hooks(tmp_path): +def test_init_project_settings_no_hooks(tmp_path, monkeypatch): """.claude/settings.json has no hooks — all hooks fire from provider level.""" + monkeypatch.setattr( + "aipass.aipass.apps.handlers.init.bootstrap.is_throwaway_path", + lambda _: False, + ) target = tmp_path / "proj" target.mkdir() @@ -451,8 +455,12 @@ def test_update_project_return_dict_structure(tmp_path): assert isinstance(result["skipped_files"], list) -def test_update_project_already_current_after_init(tmp_path): +def test_update_project_already_current_after_init(tmp_path, monkeypatch): """Running update immediately after init reports all managed files as already current.""" + monkeypatch.setattr( + "aipass.aipass.apps.handlers.init.bootstrap.is_throwaway_path", + lambda _: False, + ) target = tmp_path / "proj" target.mkdir() init_project(target, project_name="fresh") @@ -463,8 +471,12 @@ def test_update_project_already_current_after_init(tmp_path): assert len(result["already_current"]) >= 5 -def test_update_project_idempotent(tmp_path): +def test_update_project_idempotent(tmp_path, monkeypatch): """Running update twice in a row produces no changes on second run.""" + monkeypatch.setattr( + "aipass.aipass.apps.handlers.init.bootstrap.is_throwaway_path", + lambda _: False, + ) target = tmp_path / "proj" target.mkdir() init_project(target, project_name="idem") @@ -571,8 +583,12 @@ def test_init_project_returns_aipass_home(tmp_path): assert result["aipass_home"] is None or isinstance(result["aipass_home"], str) -def test_init_project_settings_has_aipass_home_when_detected(tmp_path): +def test_init_project_settings_has_aipass_home_when_detected(tmp_path, monkeypatch): """When AIPASS_HOME is detected, settings.json includes env.AIPASS_HOME.""" + monkeypatch.setattr( + "aipass.aipass.apps.handlers.init.bootstrap.is_throwaway_path", + lambda _: False, + ) target = tmp_path / "proj" target.mkdir() @@ -598,8 +614,12 @@ def test_update_project_returns_aipass_home(tmp_path): assert result["aipass_home"] is None or isinstance(result["aipass_home"], str) -def test_update_project_adds_aipass_home_if_missing(tmp_path): +def test_update_project_adds_aipass_home_if_missing(tmp_path, monkeypatch): """update_project injects AIPASS_HOME into settings.json if env section is absent.""" + monkeypatch.setattr( + "aipass.aipass.apps.handlers.init.bootstrap.is_throwaway_path", + lambda _: False, + ) target = tmp_path / "proj" target.mkdir() init_project(target, project_name="addenv") diff --git a/src/aipass/aipass/tests/test_init_flow.py b/src/aipass/aipass/tests/test_init_flow.py index 545a2b69..5d3213b6 100644 --- a/src/aipass/aipass/tests/test_init_flow.py +++ b/src/aipass/aipass/tests/test_init_flow.py @@ -261,6 +261,11 @@ def _bypass_preflight(): class TestRunInit: + @pytest.fixture(autouse=True) + def _isolate_cwd(self, tmp_path, monkeypatch): + """Avoid _guard_init rejecting the real cwd when it has .trinity/.""" + monkeypatch.chdir(tmp_path) + def _patch_all_stages(self): """Context manager that patches all 10 stage functions to no-ops.""" stage_names = [