"""Unit tests for cached worktree git probing.""" from __future__ import annotations import subprocess from app.services import worktree_probe def _result(returncode=0, stdout="", stderr=""): return subprocess.CompletedProcess( args=["git"], returncode=returncode, stdout=stdout, stderr=stderr, ) def test_probe_worktree_success(monkeypatch): worktree_probe.clear_probe_cache() calls: list[list[str]] = [] def fake_run_git(path, args): calls.append(args) if args == ["status", "--porcelain"]: return _result(stdout=" M file.py\n?? new.py\n") if args == ["rev-list", "--left-right", "--count", "HEAD...origin/main"]: return _result(stdout="2\t1\n") if args == ["log", "-1", "--format=%h %ar"]: return _result(stdout="abc1234 5 minutes ago\n") raise AssertionError(args) monkeypatch.setattr(worktree_probe, "_run_git", fake_run_git) result = worktree_probe._probe_worktree("wt-1", "/tmp/wt", "feature/wt") assert result == { "id": "wt-1", "path": "/tmp/wt", "branch": "feature/wt", "dirty_files": 2, "dirty_paths": ["file.py", "new.py"], "ahead": 2, "behind": 1, "last_commit_sha": "abc1234", "last_commit_relative": "5 minutes ago", } assert len(calls) == 3 def test_probe_worktree_cache_hit(monkeypatch): worktree_probe.clear_probe_cache() calls = 0 def fake_run_git(path, args): nonlocal calls calls += 1 if args == ["status", "--porcelain"]: return _result(stdout="") if args == ["rev-list", "--left-right", "--count", "HEAD...origin/main"]: return _result(stdout="0\t0\n") return _result(stdout="abc1234 just now\n") monkeypatch.setattr(worktree_probe, "_run_git", fake_run_git) first = worktree_probe._probe_worktree("wt-cache", "/tmp/wt", "branch") second = worktree_probe._probe_worktree("wt-cache", "/tmp/wt", "branch") assert first == second assert calls == 3 def test_probe_worktree_timeout_returns_none_and_caches(monkeypatch): worktree_probe.clear_probe_cache() calls = 0 def fake_run_git(path, args): nonlocal calls calls += 1 raise subprocess.TimeoutExpired(["git"], timeout=3) monkeypatch.setattr(worktree_probe, "_run_git", fake_run_git) assert worktree_probe._probe_worktree("wt-timeout", "/tmp/wt", "branch") is None assert worktree_probe._probe_worktree("wt-timeout", "/tmp/wt", "branch") is None assert calls == 1 def test_probe_worktree_nonzero_exit_returns_none(monkeypatch): worktree_probe.clear_probe_cache() monkeypatch.setattr( worktree_probe, "_run_git", lambda path, args: _result(returncode=128, stderr="fatal"), ) assert worktree_probe._probe_worktree("wt-fail", "/tmp/wt", "branch") is None def test_probe_worktree_malformed_output_returns_none(monkeypatch): worktree_probe.clear_probe_cache() def fake_run_git(path, args): if args == ["status", "--porcelain"]: return _result(stdout="") if args == ["rev-list", "--left-right", "--count", "HEAD...origin/main"]: return _result(stdout="not-two-fields\n") raise AssertionError(args) monkeypatch.setattr(worktree_probe, "_run_git", fake_run_git) assert worktree_probe._probe_worktree("wt-bad", "/tmp/wt", "branch") is None def test_parse_dirty_paths_basic(): output = "\n".join( [ " M cli/main.py", "?? backend/app/main.py", "A docs/notes.md", " D old.py", "MM changed.py", ] ) assert worktree_probe._parse_dirty_paths(output) == [ "cli/main.py", "backend/app/main.py", "docs/notes.md", ] def test_parse_dirty_paths_handles_renamed(): output = "R old/path.py -> new/path.py\n" assert worktree_probe._parse_dirty_paths(output) == [ "old/path.py -> new/path.py", ] def test_parse_dirty_paths_empty(): assert worktree_probe._parse_dirty_paths("") == [] def test_parse_dirty_paths_respects_limit(): output = "\n".join( [ " M one.py", " M two.py", " M three.py", " M four.py", " M five.py", " M six.py", ] ) assert worktree_probe._parse_dirty_paths(output, limit=5) == [ "one.py", "two.py", "three.py", "four.py", "five.py", ]