#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

This commit is contained in:
AIOSAI
2026-07-11 00:30:35 -07:00
parent 22b4577ec1
commit a54d21033e
3 changed files with 72 additions and 5 deletions
+42
View File
@@ -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
```
+25 -5
View File
@@ -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")
@@ -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 = [