#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:
@@ -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
|
||||
```
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user