| """Tests for tools/checkpoint_manager.py — CheckpointManager.""" |
|
|
| import logging |
| import subprocess |
| import pytest |
| from pathlib import Path |
| from unittest.mock import patch |
|
|
| from tools.checkpoint_manager import ( |
| CheckpointManager, |
| _shadow_repo_path, |
| _init_shadow_repo, |
| _run_git, |
| _git_env, |
| _dir_file_count, |
| format_checkpoint_list, |
| DEFAULT_EXCLUDES, |
| CHECKPOINT_BASE, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture() |
| def work_dir(tmp_path): |
| """Temporary working directory.""" |
| d = tmp_path / "project" |
| d.mkdir() |
| (d / "main.py").write_text("print('hello')\\n") |
| (d / "README.md").write_text("# Project\\n") |
| return d |
|
|
|
|
| @pytest.fixture() |
| def checkpoint_base(tmp_path): |
| """Isolated checkpoint base — never writes to ~/.hermes/.""" |
| return tmp_path / "checkpoints" |
|
|
|
|
| @pytest.fixture() |
| def fake_home(tmp_path, monkeypatch): |
| """Set a deterministic fake home for expanduser/path-home behavior.""" |
| home = tmp_path / "home" |
| home.mkdir() |
| monkeypatch.setenv("HOME", str(home)) |
| monkeypatch.setenv("USERPROFILE", str(home)) |
| monkeypatch.delenv("HOMEDRIVE", raising=False) |
| monkeypatch.delenv("HOMEPATH", raising=False) |
| monkeypatch.setattr(Path, "home", classmethod(lambda cls: home)) |
| return home |
|
|
|
|
| @pytest.fixture() |
| def mgr(work_dir, checkpoint_base, monkeypatch): |
| """CheckpointManager with redirected checkpoint base.""" |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| return CheckpointManager(enabled=True, max_snapshots=50) |
|
|
|
|
| @pytest.fixture() |
| def disabled_mgr(checkpoint_base, monkeypatch): |
| """Disabled CheckpointManager.""" |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| return CheckpointManager(enabled=False) |
|
|
|
|
| |
| |
| |
|
|
| class TestShadowRepoPath: |
| def test_deterministic(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| p1 = _shadow_repo_path(str(work_dir)) |
| p2 = _shadow_repo_path(str(work_dir)) |
| assert p1 == p2 |
|
|
| def test_different_dirs_different_paths(self, tmp_path, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| p1 = _shadow_repo_path(str(tmp_path / "a")) |
| p2 = _shadow_repo_path(str(tmp_path / "b")) |
| assert p1 != p2 |
|
|
| def test_under_checkpoint_base(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| p = _shadow_repo_path(str(work_dir)) |
| assert str(p).startswith(str(checkpoint_base)) |
|
|
| def test_tilde_and_expanded_home_share_shadow_repo(self, fake_home, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| project = fake_home / "project" |
| project.mkdir() |
|
|
| tilde_path = f"~/{project.name}" |
| expanded_path = str(project) |
|
|
| assert _shadow_repo_path(tilde_path) == _shadow_repo_path(expanded_path) |
|
|
|
|
| |
| |
| |
|
|
| class TestShadowRepoInit: |
| def test_creates_git_repo(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| shadow = _shadow_repo_path(str(work_dir)) |
| err = _init_shadow_repo(shadow, str(work_dir)) |
| assert err is None |
| assert (shadow / "HEAD").exists() |
|
|
| def test_no_git_in_project_dir(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| shadow = _shadow_repo_path(str(work_dir)) |
| _init_shadow_repo(shadow, str(work_dir)) |
| assert not (work_dir / ".git").exists() |
|
|
| def test_has_exclude_file(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| shadow = _shadow_repo_path(str(work_dir)) |
| _init_shadow_repo(shadow, str(work_dir)) |
| exclude = shadow / "info" / "exclude" |
| assert exclude.exists() |
| content = exclude.read_text() |
| assert "node_modules/" in content |
| assert ".env" in content |
|
|
| def test_has_workdir_file(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| shadow = _shadow_repo_path(str(work_dir)) |
| _init_shadow_repo(shadow, str(work_dir)) |
| workdir_file = shadow / "HERMES_WORKDIR" |
| assert workdir_file.exists() |
| assert str(work_dir.resolve()) in workdir_file.read_text() |
|
|
| def test_idempotent(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| shadow = _shadow_repo_path(str(work_dir)) |
| err1 = _init_shadow_repo(shadow, str(work_dir)) |
| err2 = _init_shadow_repo(shadow, str(work_dir)) |
| assert err1 is None |
| assert err2 is None |
|
|
|
|
| |
| |
| |
|
|
| class TestDisabledManager: |
| def test_ensure_checkpoint_returns_false(self, disabled_mgr, work_dir): |
| assert disabled_mgr.ensure_checkpoint(str(work_dir)) is False |
|
|
| def test_new_turn_works(self, disabled_mgr): |
| disabled_mgr.new_turn() |
|
|
|
|
| |
| |
| |
|
|
| class TestTakeCheckpoint: |
| def test_first_checkpoint(self, mgr, work_dir): |
| result = mgr.ensure_checkpoint(str(work_dir), "initial") |
| assert result is True |
|
|
| def test_successful_checkpoint_does_not_log_expected_diff_exit(self, mgr, work_dir, caplog): |
| with caplog.at_level(logging.ERROR, logger="tools.checkpoint_manager"): |
| result = mgr.ensure_checkpoint(str(work_dir), "initial") |
| assert result is True |
| assert not any("diff --cached --quiet" in r.getMessage() for r in caplog.records) |
|
|
| def test_dedup_same_turn(self, mgr, work_dir): |
| r1 = mgr.ensure_checkpoint(str(work_dir), "first") |
| r2 = mgr.ensure_checkpoint(str(work_dir), "second") |
| assert r1 is True |
| assert r2 is False |
|
|
| def test_new_turn_resets_dedup(self, mgr, work_dir): |
| r1 = mgr.ensure_checkpoint(str(work_dir), "turn 1") |
| assert r1 is True |
|
|
| mgr.new_turn() |
|
|
| |
| (work_dir / "main.py").write_text("print('modified')\\n") |
| r2 = mgr.ensure_checkpoint(str(work_dir), "turn 2") |
| assert r2 is True |
|
|
| def test_no_changes_skips_commit(self, mgr, work_dir): |
| |
| mgr.ensure_checkpoint(str(work_dir), "initial") |
| mgr.new_turn() |
|
|
| |
| r = mgr.ensure_checkpoint(str(work_dir), "no changes") |
| assert r is False |
|
|
| def test_skip_root_dir(self, mgr): |
| r = mgr.ensure_checkpoint("/", "root") |
| assert r is False |
|
|
| def test_skip_home_dir(self, mgr): |
| r = mgr.ensure_checkpoint(str(Path.home()), "home") |
| assert r is False |
|
|
|
|
| |
| |
| |
|
|
| class TestListCheckpoints: |
| def test_empty_when_no_checkpoints(self, mgr, work_dir): |
| result = mgr.list_checkpoints(str(work_dir)) |
| assert result == [] |
|
|
| def test_list_after_take(self, mgr, work_dir): |
| mgr.ensure_checkpoint(str(work_dir), "test checkpoint") |
| result = mgr.list_checkpoints(str(work_dir)) |
| assert len(result) == 1 |
| assert result[0]["reason"] == "test checkpoint" |
| assert "hash" in result[0] |
| assert "short_hash" in result[0] |
| assert "timestamp" in result[0] |
|
|
| def test_multiple_checkpoints_ordered(self, mgr, work_dir): |
| mgr.ensure_checkpoint(str(work_dir), "first") |
| mgr.new_turn() |
|
|
| (work_dir / "main.py").write_text("v2\\n") |
| mgr.ensure_checkpoint(str(work_dir), "second") |
| mgr.new_turn() |
|
|
| (work_dir / "main.py").write_text("v3\\n") |
| mgr.ensure_checkpoint(str(work_dir), "third") |
|
|
| result = mgr.list_checkpoints(str(work_dir)) |
| assert len(result) == 3 |
| |
| assert result[0]["reason"] == "third" |
| assert result[2]["reason"] == "first" |
|
|
| def test_tilde_path_lists_same_checkpoints_as_expanded_path(self, checkpoint_base, fake_home, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| mgr = CheckpointManager(enabled=True, max_snapshots=50) |
| project = fake_home / "project" |
| project.mkdir() |
| (project / "main.py").write_text("v1\n") |
|
|
| tilde_path = f"~/{project.name}" |
| assert mgr.ensure_checkpoint(tilde_path, "initial") is True |
|
|
| listed = mgr.list_checkpoints(str(project)) |
| assert len(listed) == 1 |
| assert listed[0]["reason"] == "initial" |
|
|
|
|
| |
| |
| |
|
|
| class TestRestore: |
| def test_restore_to_previous(self, mgr, work_dir): |
| |
| (work_dir / "main.py").write_text("original\\n") |
| mgr.ensure_checkpoint(str(work_dir), "original state") |
| mgr.new_turn() |
|
|
| |
| (work_dir / "main.py").write_text("modified\\n") |
|
|
| |
| checkpoints = mgr.list_checkpoints(str(work_dir)) |
| assert len(checkpoints) == 1 |
|
|
| |
| result = mgr.restore(str(work_dir), checkpoints[0]["hash"]) |
| assert result["success"] is True |
|
|
| |
| assert (work_dir / "main.py").read_text() == "original\\n" |
|
|
| def test_restore_invalid_hash(self, mgr, work_dir): |
| mgr.ensure_checkpoint(str(work_dir), "initial") |
| result = mgr.restore(str(work_dir), "deadbeef1234") |
| assert result["success"] is False |
|
|
| def test_restore_no_checkpoints(self, mgr, work_dir): |
| result = mgr.restore(str(work_dir), "abc123") |
| assert result["success"] is False |
|
|
| def test_restore_creates_pre_rollback_snapshot(self, mgr, work_dir): |
| (work_dir / "main.py").write_text("v1\\n") |
| mgr.ensure_checkpoint(str(work_dir), "v1") |
| mgr.new_turn() |
|
|
| (work_dir / "main.py").write_text("v2\\n") |
|
|
| checkpoints = mgr.list_checkpoints(str(work_dir)) |
| mgr.restore(str(work_dir), checkpoints[0]["hash"]) |
|
|
| |
| all_cps = mgr.list_checkpoints(str(work_dir)) |
| assert len(all_cps) >= 2 |
| assert "pre-rollback" in all_cps[0]["reason"] |
|
|
| def test_tilde_path_supports_diff_and_restore_flow(self, checkpoint_base, fake_home, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| mgr = CheckpointManager(enabled=True, max_snapshots=50) |
| project = fake_home / "project" |
| project.mkdir() |
| file_path = project / "main.py" |
| file_path.write_text("original\n") |
|
|
| tilde_path = f"~/{project.name}" |
| assert mgr.ensure_checkpoint(tilde_path, "initial") is True |
| mgr.new_turn() |
|
|
| file_path.write_text("changed\n") |
| checkpoints = mgr.list_checkpoints(str(project)) |
| diff_result = mgr.diff(tilde_path, checkpoints[0]["hash"]) |
| assert diff_result["success"] is True |
| assert "main.py" in diff_result["diff"] |
|
|
| restore_result = mgr.restore(tilde_path, checkpoints[0]["hash"]) |
| assert restore_result["success"] is True |
| assert file_path.read_text() == "original\n" |
|
|
|
|
| |
| |
| |
|
|
| class TestWorkingDirResolution: |
| def test_resolves_git_project_root(self, tmp_path): |
| mgr = CheckpointManager(enabled=True) |
| project = tmp_path / "myproject" |
| project.mkdir() |
| (project / ".git").mkdir() |
| subdir = project / "src" |
| subdir.mkdir() |
| filepath = subdir / "main.py" |
| filepath.write_text("x\\n") |
|
|
| result = mgr.get_working_dir_for_path(str(filepath)) |
| assert result == str(project) |
|
|
| def test_resolves_pyproject_root(self, tmp_path): |
| mgr = CheckpointManager(enabled=True) |
| project = tmp_path / "pyproj" |
| project.mkdir() |
| (project / "pyproject.toml").write_text("[project]\\n") |
| subdir = project / "src" |
| subdir.mkdir() |
|
|
| result = mgr.get_working_dir_for_path(str(subdir / "file.py")) |
| assert result == str(project) |
|
|
| def test_falls_back_to_parent(self, tmp_path): |
| mgr = CheckpointManager(enabled=True) |
| filepath = tmp_path / "random" / "file.py" |
| filepath.parent.mkdir(parents=True) |
| filepath.write_text("x\\n") |
|
|
| result = mgr.get_working_dir_for_path(str(filepath)) |
| assert result == str(filepath.parent) |
|
|
| def test_resolves_tilde_path_to_project_root(self, fake_home): |
| mgr = CheckpointManager(enabled=True) |
| project = fake_home / "myproject" |
| project.mkdir() |
| (project / "pyproject.toml").write_text("[project]\n") |
| subdir = project / "src" |
| subdir.mkdir() |
| filepath = subdir / "main.py" |
| filepath.write_text("x\n") |
|
|
| result = mgr.get_working_dir_for_path(f"~/{project.name}/src/main.py") |
| assert result == str(project) |
|
|
|
|
| |
| |
| |
|
|
| class TestGitEnvIsolation: |
| def test_sets_git_dir(self, tmp_path): |
| shadow = tmp_path / "shadow" |
| env = _git_env(shadow, str(tmp_path / "work")) |
| assert env["GIT_DIR"] == str(shadow) |
|
|
| def test_sets_work_tree(self, tmp_path): |
| shadow = tmp_path / "shadow" |
| work = tmp_path / "work" |
| env = _git_env(shadow, str(work)) |
| assert env["GIT_WORK_TREE"] == str(work.resolve()) |
|
|
| def test_clears_index_file(self, tmp_path, monkeypatch): |
| monkeypatch.setenv("GIT_INDEX_FILE", "/some/index") |
| shadow = tmp_path / "shadow" |
| env = _git_env(shadow, str(tmp_path)) |
| assert "GIT_INDEX_FILE" not in env |
|
|
| def test_expands_tilde_in_work_tree(self, fake_home, tmp_path): |
| shadow = tmp_path / "shadow" |
| work = fake_home / "work" |
| work.mkdir() |
|
|
| env = _git_env(shadow, f"~/{work.name}") |
| assert env["GIT_WORK_TREE"] == str(work.resolve()) |
|
|
|
|
| |
| |
| |
|
|
| class TestFormatCheckpointList: |
| def test_empty_list(self): |
| result = format_checkpoint_list([], "/some/dir") |
| assert "No checkpoints" in result |
|
|
| def test_formats_entries(self): |
| cps = [ |
| {"hash": "abc123", "short_hash": "abc1", "timestamp": "2026-03-09T21:15:00-07:00", "reason": "before write_file"}, |
| {"hash": "def456", "short_hash": "def4", "timestamp": "2026-03-09T21:10:00-07:00", "reason": "before patch"}, |
| ] |
| result = format_checkpoint_list(cps, "/home/user/project") |
| assert "abc1" in result |
| assert "def4" in result |
| assert "before write_file" in result |
| assert "/rollback" in result |
|
|
|
|
| |
| |
| |
|
|
| class TestDirFileCount: |
| def test_counts_files(self, work_dir): |
| count = _dir_file_count(str(work_dir)) |
| assert count >= 2 |
|
|
| def test_nonexistent_dir(self, tmp_path): |
| count = _dir_file_count(str(tmp_path / "nonexistent")) |
| assert count == 0 |
|
|
|
|
| |
| |
| |
|
|
| class TestErrorResilience: |
| def test_no_git_installed(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| mgr = CheckpointManager(enabled=True) |
| |
| monkeypatch.setattr("shutil.which", lambda x: None) |
| mgr._git_available = None |
| result = mgr.ensure_checkpoint(str(work_dir), "test") |
| assert result is False |
|
|
| def test_run_git_allows_expected_nonzero_without_error_log(self, tmp_path, caplog): |
| work = tmp_path / "work" |
| work.mkdir() |
| completed = subprocess.CompletedProcess( |
| args=["git", "diff", "--cached", "--quiet"], |
| returncode=1, |
| stdout="", |
| stderr="", |
| ) |
| with patch("tools.checkpoint_manager.subprocess.run", return_value=completed): |
| with caplog.at_level(logging.ERROR, logger="tools.checkpoint_manager"): |
| ok, stdout, stderr = _run_git( |
| ["diff", "--cached", "--quiet"], |
| tmp_path / "shadow", |
| str(work), |
| allowed_returncodes={1}, |
| ) |
| assert ok is False |
| assert stdout == "" |
| assert stderr == "" |
| assert not caplog.records |
|
|
| def test_run_git_invalid_working_dir_reports_path_error(self, tmp_path, caplog): |
| missing = tmp_path / "missing" |
| with caplog.at_level(logging.ERROR, logger="tools.checkpoint_manager"): |
| ok, stdout, stderr = _run_git( |
| ["status"], |
| tmp_path / "shadow", |
| str(missing), |
| ) |
| assert ok is False |
| assert stdout == "" |
| assert "working directory not found" in stderr |
| assert not any("Git executable not found" in r.getMessage() for r in caplog.records) |
|
|
| def test_run_git_missing_git_reports_git_not_found(self, tmp_path, monkeypatch, caplog): |
| work = tmp_path / "work" |
| work.mkdir() |
|
|
| def raise_missing_git(*args, **kwargs): |
| raise FileNotFoundError(2, "No such file or directory", "git") |
|
|
| monkeypatch.setattr("tools.checkpoint_manager.subprocess.run", raise_missing_git) |
| with caplog.at_level(logging.ERROR, logger="tools.checkpoint_manager"): |
| ok, stdout, stderr = _run_git( |
| ["status"], |
| tmp_path / "shadow", |
| str(work), |
| ) |
| assert ok is False |
| assert stdout == "" |
| assert stderr == "git not found" |
| assert any("Git executable not found" in r.getMessage() for r in caplog.records) |
|
|
| def test_checkpoint_failure_does_not_raise(self, mgr, work_dir, monkeypatch): |
| """Checkpoint failures should never raise — they're silently logged.""" |
| def broken_run_git(*args, **kwargs): |
| raise OSError("git exploded") |
| monkeypatch.setattr("tools.checkpoint_manager._run_git", broken_run_git) |
| |
| result = mgr.ensure_checkpoint(str(work_dir), "test") |
| assert result is False |
|
|
|
|
| |
| |
| |
|
|
| class TestSecurity: |
| def test_restore_rejects_argument_injection(self, mgr, work_dir): |
| mgr.ensure_checkpoint(str(work_dir), "initial") |
| |
| result = mgr.restore(str(work_dir), "--patch") |
| assert result["success"] is False |
| assert "Invalid commit hash" in result["error"] |
| assert "must not start with '-'" in result["error"] |
| |
| result = mgr.restore(str(work_dir), "-p") |
| assert result["success"] is False |
| assert "Invalid commit hash" in result["error"] |
| |
| def test_restore_rejects_invalid_hex_chars(self, mgr, work_dir): |
| mgr.ensure_checkpoint(str(work_dir), "initial") |
| |
| result = mgr.restore(str(work_dir), "abc; rm -rf /") |
| assert result["success"] is False |
| assert "expected 4-64 hex characters" in result["error"] |
| |
| result = mgr.diff(str(work_dir), "abc&def") |
| assert result["success"] is False |
| assert "expected 4-64 hex characters" in result["error"] |
|
|
| def test_restore_rejects_path_traversal(self, mgr, work_dir): |
| mgr.ensure_checkpoint(str(work_dir), "initial") |
| |
| checkpoints = mgr.list_checkpoints(str(work_dir)) |
| target_hash = checkpoints[0]["hash"] |
| |
| |
| result = mgr.restore(str(work_dir), target_hash, file_path="/etc/passwd") |
| assert result["success"] is False |
| assert "got absolute path" in result["error"] |
| |
| |
| result = mgr.restore(str(work_dir), target_hash, file_path="../outside_file.txt") |
| assert result["success"] is False |
| assert "escapes the working directory" in result["error"] |
|
|
| def test_restore_accepts_valid_file_path(self, mgr, work_dir): |
| mgr.ensure_checkpoint(str(work_dir), "initial") |
| checkpoints = mgr.list_checkpoints(str(work_dir)) |
| target_hash = checkpoints[0]["hash"] |
| |
| |
| result = mgr.restore(str(work_dir), target_hash, file_path="main.py") |
| assert result["success"] is True |
| |
| |
| (work_dir / "subdir").mkdir() |
| (work_dir / "subdir" / "test.txt").write_text("hello") |
| mgr.new_turn() |
| mgr.ensure_checkpoint(str(work_dir), "second") |
| checkpoints = mgr.list_checkpoints(str(work_dir)) |
| target_hash = checkpoints[0]["hash"] |
| |
| result = mgr.restore(str(work_dir), target_hash, file_path="subdir/test.txt") |
| assert result["success"] is True |
|
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| import os as _os |
|
|
|
|
| class TestGpgAndGlobalConfigIsolation: |
| def test_git_env_isolates_global_and_system_config(self, tmp_path): |
| """_git_env must null out GIT_CONFIG_GLOBAL / GIT_CONFIG_SYSTEM so the |
| shadow repo does not inherit user-level gpgsign, hooks, aliases, etc.""" |
| env = _git_env(tmp_path / "shadow", str(tmp_path)) |
| assert env["GIT_CONFIG_GLOBAL"] == _os.devnull |
| assert env["GIT_CONFIG_SYSTEM"] == _os.devnull |
| assert env["GIT_CONFIG_NOSYSTEM"] == "1" |
|
|
| def test_init_sets_commit_gpgsign_false(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| shadow = _shadow_repo_path(str(work_dir)) |
| _init_shadow_repo(shadow, str(work_dir)) |
| |
| |
| result = subprocess.run( |
| ["git", "config", "--file", str(shadow / "config"), "--get", "commit.gpgsign"], |
| capture_output=True, text=True, |
| ) |
| assert result.stdout.strip() == "false" |
|
|
| def test_init_sets_tag_gpgsign_false(self, work_dir, checkpoint_base, monkeypatch): |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
| shadow = _shadow_repo_path(str(work_dir)) |
| _init_shadow_repo(shadow, str(work_dir)) |
| result = subprocess.run( |
| ["git", "config", "--file", str(shadow / "config"), "--get", "tag.gpgSign"], |
| capture_output=True, text=True, |
| ) |
| assert result.stdout.strip() == "false" |
|
|
| def test_checkpoint_works_with_global_gpgsign_and_broken_gpg( |
| self, work_dir, checkpoint_base, monkeypatch, tmp_path |
| ): |
| """The real bug scenario: user has global commit.gpgsign=true but GPG |
| is broken or pinentry is unavailable. Before the fix, every snapshot |
| either failed or spawned a pinentry window. After the fix, snapshots |
| succeed without ever invoking GPG.""" |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
|
|
| |
| |
| |
| fake_home = tmp_path / "fake_home" |
| fake_home.mkdir() |
| (fake_home / ".gitconfig").write_text( |
| "[user]\n email = real@user.com\n name = Real User\n" |
| "[commit]\n gpgsign = true\n" |
| "[tag]\n gpgSign = true\n" |
| "[gpg]\n program = /nonexistent/fake-gpg-binary\n" |
| ) |
| monkeypatch.setenv("HOME", str(fake_home)) |
| monkeypatch.delenv("GPG_TTY", raising=False) |
| monkeypatch.delenv("DISPLAY", raising=False) |
|
|
| mgr = CheckpointManager(enabled=True) |
| assert mgr.ensure_checkpoint(str(work_dir), reason="with-global-gpgsign") is True |
| assert len(mgr.list_checkpoints(str(work_dir))) == 1 |
|
|
| def test_checkpoint_works_on_prefix_shadow_without_local_gpgsign( |
| self, work_dir, checkpoint_base, monkeypatch, tmp_path |
| ): |
| """Users with shadow repos created before the fix will not have |
| commit.gpgsign=false in their shadow's own config. The inline |
| ``--no-gpg-sign`` flag on the commit call must cover them.""" |
| monkeypatch.setattr("tools.checkpoint_manager.CHECKPOINT_BASE", checkpoint_base) |
|
|
| |
| |
| |
| shadow = _shadow_repo_path(str(work_dir)) |
| _init_shadow_repo(shadow, str(work_dir)) |
| subprocess.run( |
| ["git", "config", "--file", str(shadow / "config"), |
| "--unset", "commit.gpgsign"], |
| capture_output=True, text=True, check=False, |
| ) |
| subprocess.run( |
| ["git", "config", "--file", str(shadow / "config"), |
| "--unset", "tag.gpgSign"], |
| capture_output=True, text=True, check=False, |
| ) |
|
|
| |
| fake_home = tmp_path / "fake_home" |
| fake_home.mkdir() |
| (fake_home / ".gitconfig").write_text( |
| "[commit]\n gpgsign = true\n" |
| "[gpg]\n program = /nonexistent/fake-gpg-binary\n" |
| ) |
| monkeypatch.setenv("HOME", str(fake_home)) |
| monkeypatch.delenv("GPG_TTY", raising=False) |
| monkeypatch.delenv("DISPLAY", raising=False) |
|
|
| mgr = CheckpointManager(enabled=True) |
| assert mgr.ensure_checkpoint(str(work_dir), reason="prefix-shadow") is True |
| assert len(mgr.list_checkpoints(str(work_dir))) == 1 |
|
|