test: sandbox HOME for the whole suite so installers can't touch real config (#2168)

Several test files call install/uninstall functions that operate on the
real user home (~/.claude, ~/.gemini, ~/.codebuddy, ~/.copilot), so
running the suite deleted/overwrote the developer's actual config. An
autouse conftest fixture now points HOME/USERPROFILE/LOCALAPPDATA at a
throwaway dir and clears CLAUDE_CONFIG_DIR/XDG_CONFIG_HOME for every
test. Supersedes the per-file sandbox proposed in #2057 (thanks
@erlandl4g for surfacing it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-07-25 18:02:33 +01:00
co-authored by Claude Opus 4.8
parent 33aa89c722
commit c18ec81741
2 changed files with 77 additions and 0 deletions
+19
View File
@@ -1,9 +1,28 @@
from __future__ import annotations
from pathlib import Path
from typing import Any
import pytest
@pytest.fixture(autouse=True)
def _sandbox_home(tmp_path_factory, monkeypatch):
"""Every test gets a throwaway HOME so installers/uninstallers can never
touch the developer's real ~/.claude, ~/.gemini, ~/.codebuddy, ~/.copilot,
~/.config, ~/.agents (issue #2168).
Allocated via tmp_path_factory (not inside tmp_path) so tests that assert
the exact contents of their own tmp_path are unaffected."""
home = tmp_path_factory.mktemp("sandbox-home")
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home)) # Windows ntpath.expanduser
monkeypatch.setenv("LOCALAPPDATA", str(home / "AppData" / "Local"))
monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False) # escape hatch that bypasses Path.home
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
monkeypatch.setattr(Path, "home", classmethod(lambda cls: home))
return home
_ANALYZE_WARNING_FILTERS = (
"ignore:Tensorflow not installed; ParametricUMAP will be unavailable:ImportWarning:umap",
"ignore:Please import `random` from the `scipy\\.sparse` namespace.*:"
+58
View File
@@ -0,0 +1,58 @@
"""Regression tests for the repo-wide HOME sandbox (issue #2168).
The autouse ``_sandbox_home`` fixture in conftest.py must point every
home-resolution mechanism at a throwaway directory, so installers and
uninstallers exercised by the suite can never delete or rewrite the
developer's real ~/.claude, ~/.gemini, ~/.codebuddy, ~/.copilot, etc.
"""
from __future__ import annotations
import os
from pathlib import Path
from graphify.__main__ import claude_uninstall
# Module import happens during collection, before any fixture runs, so this
# captures the developer's actual home directory for comparison below.
_REAL_HOME = Path(os.path.realpath(os.path.expanduser("~")))
def test_path_home_is_sandboxed(tmp_path_factory):
home = Path(os.path.realpath(Path.home()))
assert home != _REAL_HOME
assert not home.is_relative_to(_REAL_HOME / ".claude")
assert home.name.startswith("sandbox-home")
basetemp = Path(os.path.realpath(tmp_path_factory.getbasetemp()))
assert home.is_relative_to(basetemp)
def test_expanduser_is_sandboxed(tmp_path_factory):
expanded = Path(os.path.realpath(os.path.expanduser("~")))
assert expanded != _REAL_HOME
basetemp = Path(os.path.realpath(tmp_path_factory.getbasetemp()))
assert expanded.is_relative_to(basetemp)
def test_claude_config_dir_escape_hatch_is_cleared():
assert "CLAUDE_CONFIG_DIR" not in os.environ
assert "XDG_CONFIG_HOME" not in os.environ
def test_global_uninstall_is_captured_by_sandbox(tmp_path, tmp_path_factory):
"""claude_uninstall deletes the *global* ~/.claude/skills/graphify tree.
Plant that tree inside the sandbox home, run uninstall against an
unrelated project dir, and prove the delete landed in the sandbox
(and therefore not in the developer's real home).
"""
skill = Path.home() / ".claude" / "skills" / "graphify" / "SKILL.md"
skill.parent.mkdir(parents=True)
skill.write_text("# graphify skill (sandbox copy)\n", encoding="utf-8")
project_dir = tmp_path / "some-project"
project_dir.mkdir()
claude_uninstall(project_dir)
assert not skill.exists(), "global skill delete was not captured by the sandbox"
# And the sandbox home itself is still inside pytest's tmp area.
assert Path.home().is_relative_to(tmp_path_factory.getbasetemp())