1+ import pytest
2+ import asyncio
3+ import time
4+ from pathlib import Path
5+ from unittest .mock import MagicMock , AsyncMock , patch
6+
7+ # Make sure these import paths are correct for your project structure
8+ from forklet .core .orchestrator import DownloadOrchestrator
9+ from forklet .models import GitHubFile , DownloadStatus
10+
11+ # --- Test Fixtures for Setup ---
12+
13+ @pytest .fixture
14+ def mock_services ():
15+ """Creates mock objects for services used by the orchestrator."""
16+ github_service = MagicMock ()
17+ download_service = MagicMock ()
18+ github_service .get_repository_tree = AsyncMock ()
19+ github_service .get_file_content = AsyncMock ()
20+ download_service .save_content = AsyncMock (return_value = 128 )
21+ download_service .ensure_directory = AsyncMock ()
22+ return github_service , download_service
23+
24+ @pytest .fixture
25+ def orchestrator (mock_services ):
26+ """Initializes the DownloadOrchestrator with mocked services."""
27+ github_service , download_service = mock_services
28+ orchestrator_instance = DownloadOrchestrator (
29+ github_service = github_service ,
30+ download_service = download_service ,
31+ max_concurrent_downloads = 5
32+ )
33+ orchestrator_instance .reset_state ()
34+ return orchestrator_instance
35+
36+ @pytest .fixture
37+ def mock_request ():
38+ """Creates a mock DownloadRequest object for use in tests."""
39+ request = MagicMock ()
40+ request .repository .owner = "test-owner"
41+ request .repository .name = "test-repo"
42+ request .repository .display_name = "test-owner/test-repo"
43+ request .git_ref = "main"
44+ request .filters = MagicMock ()
45+ request .filters .include_patterns = []
46+ request .filters .exclude_patterns = []
47+ request .destination = Path ("/fake/destination" )
48+ request .create_destination = True
49+ request .overwrite_existing = False
50+ request .preserve_structure = True
51+ request .show_progress_bars = False
52+ return request
53+
54+ # --- Test Cases ---
55+
56+ class TestDownloadOrchestrator :
57+
58+ def test_initialization_sets_properties_correctly (self , orchestrator ):
59+ """Verify that max_concurrent_downloads is correctly set."""
60+ assert orchestrator .max_concurrent_downloads == 5
61+ assert orchestrator ._semaphore ._value == 5
62+ assert not orchestrator ._is_cancelled
63+
64+ @pytest .mark .asyncio
65+ async def test_execute_download_success (self , orchestrator , mock_services , mock_request ):
66+ """Simulate a successful download with mocked services."""
67+ github_service , _ = mock_services
68+ mock_file_list = [MagicMock (spec = GitHubFile , path = "file1.txt" , size = 100 )]
69+ github_service .get_repository_tree .return_value = mock_file_list
70+
71+ with patch .object (orchestrator , '_download_files_concurrently' , new_callable = AsyncMock ) as mock_downloader , \
72+ patch ('forklet.core.orchestrator.FilterEngine' ) as mock_filter_engine :
73+
74+ mock_downloader .return_value = (["file1.txt" ], {})
75+ mock_filter_engine .return_value .filter_files .return_value .included_files = mock_file_list
76+
77+ result = await orchestrator .execute_download (request = mock_request )
78+
79+ mock_downloader .assert_awaited_once ()
80+ assert result .status == DownloadStatus .COMPLETED
81+
82+ @pytest .mark .asyncio
83+ async def test_execute_download_repo_fetch_fails (self , orchestrator , mock_services , mock_request ):
84+ """Test error handling when repository tree fetch fails."""
85+ github_service , _ = mock_services
86+ github_service .get_repository_tree .side_effect = Exception ("API limit reached" )
87+
88+ result = await orchestrator .execute_download (request = mock_request )
89+
90+ assert result .status == DownloadStatus .FAILED
91+ assert "API limit reached" in result .error_message
92+
93+ def test_cancel_sets_flag_and_logs (self , orchestrator ):
94+ """Test cancel() -> sets _is_cancelled=True and logs when a download is active."""
95+ orchestrator ._current_result = MagicMock ()
96+
97+ with patch ('forklet.core.orchestrator.logger' ) as mock_logger :
98+ orchestrator .cancel ()
99+ assert orchestrator ._is_cancelled is True
100+ mock_logger .info .assert_called_with ("Download cancelled by user" )
101+
102+ @pytest .mark .asyncio
103+ async def test_pause_and_resume_flow (self , orchestrator , mock_services , mock_request ):
104+ """Tests the full pause and resume flow in a stable, controlled manner."""
105+ github_service , _ = mock_services
106+ # --- THIS IS THE FIX ---
107+ # The mock file MUST have a 'size' attribute for the sum() calculation.
108+ mock_file_list = [MagicMock (spec = GitHubFile , path = "file1.txt" , size = 100 )]
109+ # -----------------------
110+ github_service .get_repository_tree .return_value = mock_file_list
111+
112+ download_can_complete = asyncio .Event ()
113+
114+ async def wait_for_signal_to_finish (* args , ** kwargs ):
115+ await download_can_complete .wait ()
116+ return (["file1.txt" ], {})
117+
118+ with patch .object (orchestrator , '_download_files_concurrently' , side_effect = wait_for_signal_to_finish ), \
119+ patch ('forklet.core.orchestrator.FilterEngine' ) as mock_filter_engine :
120+
121+ mock_filter_engine .return_value .filter_files .return_value .included_files = mock_file_list
122+
123+ download_task = asyncio .create_task (orchestrator .execute_download (mock_request ))
124+
125+ await asyncio .sleep (0.01 )
126+
127+ if download_task .done () and download_task .exception ():
128+ raise download_task .exception ()
129+
130+ assert orchestrator ._current_result is not None , "Orchestrator._current_result was not set."
131+
132+ await orchestrator .pause ()
133+ assert orchestrator ._is_paused is True
134+ assert orchestrator ._current_result .status == DownloadStatus .PAUSED
135+
136+ await orchestrator .resume ()
137+ assert orchestrator ._is_paused is False
138+ assert orchestrator ._current_result .status == DownloadStatus .IN_PROGRESS
139+
140+ download_can_complete .set ()
141+
142+ final_result = await download_task
143+ assert final_result .status == DownloadStatus .COMPLETED
144+
145+ def test_get_current_progress_returns_none_when_inactive (self , orchestrator ):
146+ """Test get_current_progress() -> returns None when no download is active."""
147+ assert orchestrator .get_current_progress () is None
0 commit comments