33import asyncio
44from pathlib import Path
55from subprocess import CompletedProcess
6- from unittest .mock import AsyncMock
6+ from unittest .mock import AsyncMock , patch
77
88import pytest
99
@@ -26,13 +26,8 @@ def mock_subprocess_exec(monkeypatch) -> AsyncMock:
2626@pytest .fixture
2727def mock_execute_git (monkeypatch ) -> AsyncMock :
2828 """Fixture to mock execute_git_command."""
29-
30- async def side_effect (* args , ** kwargs ):
31- if args [0 ] == "clone" :
32- return CompletedProcess (args , 0 , "Cloning..." , "" )
33- return CompletedProcess (args , 0 , "stdout success" , "" )
34-
35- mock = AsyncMock (side_effect = side_effect )
29+ # Remove the side_effect to allow tests to set their own return_value
30+ mock = AsyncMock ()
3631 monkeypatch .setattr (git_module , "execute_git_command" , mock )
3732 return mock
3833
@@ -48,6 +43,9 @@ async def test_managed_repo_clone_bare_new(
4843):
4944 """Test clone_bare when the repository does not exist yet."""
5045 repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
46+ mock_execute_git .return_value = CompletedProcess (
47+ args = [], returncode = 0 , stdout = "" , stderr = ""
48+ )
5149 # Simulate repo does not exist
5250 monkeypatch .setattr (Path , "exists" , lambda self : False )
5351
@@ -70,6 +68,9 @@ async def test_managed_repo_clone_bare_exists(
7068):
7169 """Test clone_bare when the repository already exists."""
7270 repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
71+ mock_execute_git .return_value = CompletedProcess (
72+ args = [], returncode = 0 , stdout = "" , stderr = ""
73+ )
7374 # Simulate repo exists
7475 monkeypatch .setattr (Path , "exists" , lambda self : True )
7576
@@ -94,6 +95,9 @@ async def test_managed_repo_create_worktree_success(
9495 tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
9596):
9697 """Test create_worktree successfully creates a worktree."""
98+ mock_execute_git .return_value = CompletedProcess (
99+ args = [], returncode = 0 , stdout = "" , stderr = ""
100+ )
97101 repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
98102 # Simulate bare repo exists
99103 monkeypatch .setattr (Path , "exists" , lambda self : True )
@@ -117,6 +121,9 @@ async def test_managed_repo_create_worktree_with_options(
117121 tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
118122):
119123 """Test create_worktree with additional clone options."""
124+ mock_execute_git .return_value = CompletedProcess (
125+ args = [], returncode = 0 , stdout = "" , stderr = ""
126+ )
120127 repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
121128 # Simulate bare repo exists
122129 monkeypatch .setattr (Path , "exists" , lambda self : True )
@@ -157,6 +164,9 @@ async def test_managed_repo_create_worktree_not_local(
157164 tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
158165):
159166 """Test create_worktree without the --local flag."""
167+ mock_execute_git .return_value = CompletedProcess (
168+ args = [], returncode = 0 , stdout = "" , stderr = ""
169+ )
160170 repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
161171 # Simulate bare repo exists
162172 monkeypatch .setattr (Path , "exists" , lambda self : True )
@@ -218,6 +228,9 @@ async def test_fetch_updates_success(
218228 tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
219229):
220230 """Test fetch_updates successfully fetches updates."""
231+ mock_execute_git .return_value = CompletedProcess (
232+ args = [], returncode = 0 , stdout = "" , stderr = ""
233+ )
221234 repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
222235 # Simulate bare repo exists
223236 monkeypatch .setattr (Path , "exists" , lambda self : True )
@@ -260,6 +273,9 @@ async def test_managed_repo_clone_bare_already_processed(
260273 tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
261274):
262275 """Test clone_bare when the repository has been processed in this run."""
276+ mock_execute_git .return_value = CompletedProcess (
277+ args = [], returncode = 0 , stdout = "" , stderr = ""
278+ )
263279 repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
264280 # Simulate repo does not exist for the first call
265281 monkeypatch .setattr (Path , "exists" , lambda self : False )
@@ -284,3 +300,103 @@ async def test_managed_repo_clone_bare_already_processed(
284300 assert result2 is True
285301 # The mock should still have only been called once
286302 mock_execute_git .assert_awaited_once ()
303+
304+
305+ async def test_ls_tree_no_repo (
306+ tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
307+ ):
308+ """Test ls_tree when the bare repository does not exist."""
309+ repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
310+ # Simulate bare repo does not exist
311+ monkeypatch .setattr (Path , "exists" , lambda self : False )
312+
313+ with patch .object (git_module .log , "warning" ) as mock_log_warning :
314+ result = await repo .ls_tree ("main" )
315+
316+ assert result == []
317+ mock_log_warning .assert_called_once_with (
318+ "Cannot run ls-tree: Bare repository does not exist at %s" ,
319+ repo .bare_repo_path ,
320+ )
321+ mock_execute_git .assert_not_awaited ()
322+
323+
324+ async def test_ls_tree_success_recursive (
325+ tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
326+ ):
327+ """Test ls_tree successfully lists files recursively."""
328+ repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
329+ monkeypatch .setattr (Path , "exists" , lambda self : True )
330+ mock_execute_git .return_value = CompletedProcess (
331+ args = [], returncode = 0 , stdout = "file1.txt\n dir/file2.txt" , stderr = ""
332+ )
333+
334+ result = await repo .ls_tree ("main" )
335+
336+ assert result == ["file1.txt" , "dir/file2.txt" ]
337+ mock_execute_git .assert_awaited_once_with (
338+ "ls-tree" ,
339+ "--name-only" ,
340+ "-r" ,
341+ "main" ,
342+ cwd = repo .bare_repo_path ,
343+ gitconfig = None ,
344+ )
345+
346+
347+ async def test_ls_tree_success_non_recursive (
348+ tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
349+ ):
350+ """Test ls_tree successfully lists files non-recursively."""
351+ repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
352+ monkeypatch .setattr (Path , "exists" , lambda self : True )
353+ mock_execute_git .return_value = CompletedProcess (
354+ args = [], returncode = 0 , stdout = "file1.txt" , stderr = ""
355+ )
356+
357+ result = await repo .ls_tree ("main" , recursive = False )
358+
359+ assert result == ["file1.txt" ]
360+ mock_execute_git .assert_awaited_once_with (
361+ "ls-tree" ,
362+ "--name-only" ,
363+ "main" ,
364+ cwd = repo .bare_repo_path ,
365+ gitconfig = None ,
366+ )
367+
368+
369+ async def test_ls_tree_empty_output (
370+ tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
371+ ):
372+ """Test ls_tree handles empty stdout correctly."""
373+ repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
374+ monkeypatch .setattr (Path , "exists" , lambda self : True )
375+ mock_execute_git .return_value = CompletedProcess (
376+ args = [], returncode = 0 , stdout = "" , stderr = ""
377+ )
378+
379+ result = await repo .ls_tree ("main" )
380+
381+ assert result == []
382+
383+
384+ async def test_ls_tree_command_failure (
385+ tmp_path : Path , mock_execute_git : AsyncMock , monkeypatch
386+ ):
387+ """Test ls_tree handles a RuntimeError from execute_git_command."""
388+ repo = ManagedGitRepo ("http://a.b/org/c.git" , tmp_path )
389+ monkeypatch .setattr (Path , "exists" , lambda self : True )
390+ error = RuntimeError ("Git command failed" )
391+ mock_execute_git .side_effect = error
392+
393+ with patch .object (git_module .log , "error" ) as mock_log_error :
394+ result = await repo .ls_tree ("develop" )
395+
396+ assert result == []
397+ mock_log_error .assert_called_once_with (
398+ "Failed to run ls-tree on branch '%s' in '%s': %s" ,
399+ "develop" ,
400+ repo .slug ,
401+ error ,
402+ )
0 commit comments