11# SPDX-License-Identifier: MIT
22"""Tests for the CLI module."""
33
4+ import asyncio
45import json
56import tempfile
67from pathlib import Path
7- from unittest .mock import AsyncMock , Mock , patch
8+ from unittest .mock import AsyncMock , MagicMock , Mock , patch
89
910import pytest
1011from click .testing import CliRunner
@@ -24,6 +25,32 @@ def runner():
2425 return CliRunner ()
2526
2627
28+ @pytest .fixture (autouse = True )
29+ def mock_cache_sync_manager ():
30+ """Mock cache_sync_manager to prevent unawaited coroutine warnings.
31+
32+ This fixture is autouse=True so it applies to all tests in this module,
33+ preventing the real cache_sync_manager from being accessed and creating
34+ unawaited coroutines during test execution.
35+ """
36+
37+ # Create a real async function that can be awaited
38+ async def _mock_sync_impl (* args , ** kwargs ):
39+ """Mock implementation of sync_cache_with_config."""
40+ return {}
41+
42+ # Create a mock manager with all required methods
43+ mock_manager = MagicMock ()
44+ # Assign the actual async function (not AsyncMock) to avoid introspection issues
45+ mock_manager .sync_cache_with_config = _mock_sync_impl
46+ mock_manager .get_sync_status = MagicMock (
47+ return_value = {"sync_in_progress" : False , "backends" : {}}
48+ )
49+
50+ with patch ("aletheia_probe.cli.cache_sync_manager" , mock_manager ):
51+ yield mock_manager
52+
53+
2754@pytest .fixture
2855def mock_assessment_result ():
2956 """Create mock assessment result."""
@@ -54,8 +81,15 @@ class TestAssessCommand:
5481
5582 def test_assess_basic_usage (self , runner , mock_assessment_result ):
5683 """Test basic journal command usage."""
84+ # Store the real asyncio.run before patching
85+ real_asyncio_run = asyncio .run
86+
87+ def run_coro (coro ):
88+ """Run the coroutine using real asyncio.run."""
89+ return real_asyncio_run (coro )
90+
5791 with (
58- patch ("aletheia_probe.cli.asyncio.run" ) as mock_run ,
92+ patch ("aletheia_probe.cli.asyncio.run" , side_effect = run_coro ) as mock_run ,
5993 patch ("aletheia_probe.cli._async_assess_publication" ) as mock_async_assess ,
6094 ):
6195 mock_async_assess .return_value = None
@@ -67,10 +101,19 @@ def test_assess_basic_usage(self, runner, mock_assessment_result):
67101
68102 def test_assess_with_verbose_flag (self , runner ):
69103 """Test journal command with verbose flag."""
104+ # Store the real asyncio.run before patching
105+ real_asyncio_run = asyncio .run
106+
107+ def run_coro (coro ):
108+ """Run the coroutine using real asyncio.run."""
109+ return real_asyncio_run (coro )
110+
70111 with (
71- patch ("aletheia_probe.cli.asyncio.run" ) as mock_run ,
112+ patch ("aletheia_probe.cli.asyncio.run" , side_effect = run_coro ) as mock_run ,
72113 patch ("aletheia_probe.cli._async_assess_publication" ) as mock_async_assess ,
73114 ):
115+ mock_async_assess .return_value = None
116+
74117 result = runner .invoke (main , ["journal" , "Test Journal" , "--verbose" ])
75118
76119 assert result .exit_code == 0
@@ -79,10 +122,19 @@ def test_assess_with_verbose_flag(self, runner):
79122
80123 def test_assess_with_json_format (self , runner ):
81124 """Test journal command with JSON output format."""
125+ # Store the real asyncio.run before patching
126+ real_asyncio_run = asyncio .run
127+
128+ def run_coro (coro ):
129+ """Run the coroutine using real asyncio.run."""
130+ return real_asyncio_run (coro )
131+
82132 with (
83- patch ("aletheia_probe.cli.asyncio.run" ) as mock_run ,
133+ patch ("aletheia_probe.cli.asyncio.run" , side_effect = run_coro ) as mock_run ,
84134 patch ("aletheia_probe.cli._async_assess_publication" ) as mock_async_assess ,
85135 ):
136+ mock_async_assess .return_value = None
137+
86138 result = runner .invoke (
87139 main , ["journal" , "Test Journal" , "--format" , "json" ]
88140 )
@@ -137,12 +189,12 @@ def test_sync_command_success(self, runner):
137189 "backend2" : {"status" : "current" },
138190 }
139191
140- with (
141- patch ("aletheia_probe.cli.asyncio.run" ) as mock_run ,
142- patch ("aletheia_probe.cli.cache_sync_manager" ) as mock_cache_sync ,
143- ):
144- mock_run .return_value = mock_sync_result
192+ def run_coro (coro ):
193+ """Run coroutine and return mock result."""
194+ coro .close () # Close without running
195+ return mock_sync_result
145196
197+ with patch ("aletheia_probe.cli.asyncio.run" , side_effect = run_coro ) as mock_run :
146198 result = runner .invoke (main , ["sync" ])
147199
148200 assert result .exit_code == 0
@@ -151,12 +203,13 @@ def test_sync_command_success(self, runner):
151203
152204 def test_sync_command_with_force (self , runner ):
153205 """Test sync command with force flag."""
154- with (
155- patch ("aletheia_probe.cli.asyncio.run" ) as mock_run ,
156- patch ("aletheia_probe.cli.cache_sync_manager" ) as mock_cache_sync ,
157- ):
158- mock_run .return_value = {}
159206
207+ def run_coro (coro ):
208+ """Run coroutine and return empty dict."""
209+ coro .close () # Close without running
210+ return {}
211+
212+ with patch ("aletheia_probe.cli.asyncio.run" , side_effect = run_coro ) as mock_run :
160213 result = runner .invoke (main , ["sync" , "--force" ])
161214
162215 assert result .exit_code == 0
@@ -167,9 +220,12 @@ def test_sync_command_skipped(self, runner):
167220 """Test sync command when sync is skipped."""
168221 mock_sync_result = {"status" : "skipped" , "reason" : "auto_sync_disabled" }
169222
170- with patch ("aletheia_probe.cli.asyncio.run" ) as mock_run :
171- mock_run .return_value = mock_sync_result
223+ def run_coro (coro ):
224+ """Run coroutine and return mock result."""
225+ coro .close () # Close without running
226+ return mock_sync_result
172227
228+ with patch ("aletheia_probe.cli.asyncio.run" , side_effect = run_coro ) as mock_run :
173229 result = runner .invoke (main , ["sync" ])
174230
175231 assert result .exit_code == 0
@@ -178,9 +234,13 @@ def test_sync_command_skipped(self, runner):
178234
179235 def test_sync_command_error (self , runner ):
180236 """Test sync command with error."""
181- with patch ("aletheia_probe.cli.asyncio.run" ) as mock_run :
182- mock_run .side_effect = Exception ("Sync failed" )
183237
238+ def mock_run_with_cleanup (coro ):
239+ """Mock asyncio.run that properly closes coroutines before raising."""
240+ coro .close () # Close the coroutine to avoid warning
241+ raise Exception ("Sync failed" )
242+
243+ with patch ("aletheia_probe.cli.asyncio.run" , side_effect = mock_run_with_cleanup ):
184244 result = runner .invoke (main , ["sync" ])
185245
186246 assert result .exit_code == 1
@@ -191,7 +251,7 @@ def test_sync_command_error(self, runner):
191251class TestStatusCommand :
192252 """Test cases for the status command."""
193253
194- def test_status_command_success (self , runner ):
254+ def test_status_command_success (self , runner , mock_cache_sync_manager ):
195255 """Test status command successful execution."""
196256 mock_status = {
197257 "sync_in_progress" : False ,
@@ -206,37 +266,34 @@ def test_status_command_success(self, runner):
206266 },
207267 }
208268
209- with patch ("aletheia_probe.cli.cache_sync_manager" ) as mock_cache_sync :
210- mock_cache_sync .get_sync_status .return_value = mock_status
269+ mock_cache_sync_manager .get_sync_status .return_value = mock_status
211270
212- result = runner .invoke (main , ["status" ])
271+ result = runner .invoke (main , ["status" ])
213272
214- assert result .exit_code == 0
215- assert "backend1" in result .output
216- assert "enabled" in result .output
217- assert "disabled" in result .output
273+ assert result .exit_code == 0
274+ assert "backend1" in result .output
275+ assert "enabled" in result .output
276+ assert "disabled" in result .output
218277
219- def test_status_command_sync_in_progress (self , runner ):
278+ def test_status_command_sync_in_progress (self , runner , mock_cache_sync_manager ):
220279 """Test status command when sync is in progress."""
221280 mock_status = {"sync_in_progress" : True , "backends" : {}}
222281
223- with patch ("aletheia_probe.cli.cache_sync_manager" ) as mock_cache_sync :
224- mock_cache_sync .get_sync_status .return_value = mock_status
282+ mock_cache_sync_manager .get_sync_status .return_value = mock_status
225283
226- result = runner .invoke (main , ["status" ])
284+ result = runner .invoke (main , ["status" ])
227285
228- assert result .exit_code == 0
229- assert "progress" in result .output .lower ()
286+ assert result .exit_code == 0
287+ assert "progress" in result .output .lower ()
230288
231- def test_status_command_error (self , runner ):
289+ def test_status_command_error (self , runner , mock_cache_sync_manager ):
232290 """Test status command with error."""
233- with patch ("aletheia_probe.cli.cache_sync_manager" ) as mock_cache_sync :
234- mock_cache_sync .get_sync_status .side_effect = Exception ("Status error" )
291+ mock_cache_sync_manager .get_sync_status .side_effect = Exception ("Status error" )
235292
236- result = runner .invoke (main , ["status" ])
293+ result = runner .invoke (main , ["status" ])
237294
238- assert result .exit_code == 1
239- assert "Status error" in result .output
295+ assert result .exit_code == 1
296+ assert "Status error" in result .output
240297
241298
242299class TestAddListCommand :
@@ -249,11 +306,7 @@ def test_add_list_success(self, runner):
249306 temp_file = f .name
250307
251308 try :
252- with (
253- patch ("aletheia_probe.cli.data_updater" ) as mock_updater ,
254- patch ("aletheia_probe.cli.asyncio.run" ) as mock_run ,
255- patch ("aletheia_probe.cli.cache_sync_manager" ),
256- ):
309+ with patch ("aletheia_probe.cli.data_updater" ) as mock_updater :
257310 result = runner .invoke (
258311 main ,
259312 [
0 commit comments