Coverage for tests / unit / utils / test_tool_executor_ai.py: 96%
81 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tests for AI-specific behavior in tool executor."""
3from __future__ import annotations
5from typing import Any
6from unittest.mock import MagicMock
8from assertpy import assert_that
10import lintro.utils.tool_executor as te
11from lintro.ai.config import AIConfig
12from lintro.config.execution_config import ExecutionConfig
13from lintro.config.lintro_config import LintroConfig
14from lintro.enums.action import Action
15from lintro.models.core.tool_result import ToolResult
16from lintro.utils.execution.tool_configuration import ToolsToRunResult
17from lintro.utils.tool_executor import _warn_ai_fix_disabled, run_lint_tools_simple
19# ---------------------------------------------------------------------------
20# _warn_ai_fix_disabled
21# ---------------------------------------------------------------------------
24def test_warn_ai_fix_disabled_warns_only_for_check_when_fix_requested_and_ai_disabled():
25 """Warn when action is CHECK, ai_fix=True, and AI disabled."""
26 logger = MagicMock()
28 _warn_ai_fix_disabled(
29 action=Action.CHECK,
30 ai_fix=True,
31 ai_enabled=False,
32 logger=logger,
33 )
35 assert_that(logger.console_output.call_count).is_equal_to(1)
36 warning_text = logger.console_output.call_args[0][0]
37 assert_that(warning_text).contains("AI fixes requested")
38 assert_that(warning_text).contains("ai.enabled is false")
41def test_warn_ai_fix_disabled_no_warning_for_other_states():
42 """Test that no warning is issued for non-qualifying state combinations."""
43 logger = MagicMock()
45 _warn_ai_fix_disabled(
46 action=Action.FIX,
47 ai_fix=True,
48 ai_enabled=False,
49 logger=logger,
50 )
51 _warn_ai_fix_disabled(
52 action=Action.CHECK,
53 ai_fix=False,
54 ai_enabled=False,
55 logger=logger,
56 )
57 _warn_ai_fix_disabled(
58 action=Action.CHECK,
59 ai_fix=True,
60 ai_enabled=True,
61 logger=logger,
62 )
64 assert_that(logger.console_output.call_count).is_equal_to(0)
67def test_warn_ai_fix_disabled_suppressed_for_json_output():
68 """Warning is suppressed when output format is JSON."""
69 logger = MagicMock()
71 _warn_ai_fix_disabled(
72 action=Action.CHECK,
73 ai_fix=True,
74 ai_enabled=False,
75 logger=logger,
76 output_format="json",
77 )
79 assert_that(logger.console_output.call_count).is_equal_to(0)
82def test_warn_ai_fix_disabled_suppressed_for_sarif_output():
83 """Warning is suppressed when output format is SARIF."""
84 logger = MagicMock()
86 _warn_ai_fix_disabled(
87 action=Action.CHECK,
88 ai_fix=True,
89 ai_enabled=False,
90 logger=logger,
91 output_format="sarif",
92 )
94 assert_that(logger.console_output.call_count).is_equal_to(0)
97# ---------------------------------------------------------------------------
98# Post-AI total recalculation
99# ---------------------------------------------------------------------------
102def test_fix_recomputes_totals_after_ai_changes(monkeypatch, fake_logger):
103 """Test that fix recomputes totals after AI changes."""
105 class _FakeTool:
106 def set_options(self, **_kwargs: Any) -> None:
107 return None
109 def reset_options(self) -> None:
110 return None
112 def fix(self, _paths: Any, _options: Any) -> ToolResult:
113 return ToolResult(
114 name="ruff",
115 success=False,
116 issues_count=1,
117 fixed_issues_count=0,
118 remaining_issues_count=1,
119 issues=[],
120 )
122 def check(self, _paths: Any, _options: Any) -> ToolResult:
123 return ToolResult(
124 name="ruff",
125 success=False,
126 issues_count=1,
127 issues=[],
128 )
130 lintro_config = LintroConfig(
131 execution=ExecutionConfig(parallel=False),
132 ai=AIConfig(
133 enabled=True,
134 auto_apply=True,
135 ),
136 )
138 monkeypatch.setattr(
139 te,
140 "get_tools_to_run",
141 lambda tools, action, **_kw: ToolsToRunResult(to_run=["ruff"]),
142 )
143 monkeypatch.setattr(
144 te.tool_manager, # type: ignore[attr-defined] # singleton
145 "get_tool",
146 lambda name: _FakeTool(),
147 )
148 monkeypatch.setattr(
149 te,
150 "configure_tool_for_execution",
151 lambda **kwargs: None,
152 )
153 monkeypatch.setattr(
154 te,
155 "execute_post_checks",
156 lambda **kwargs: (
157 kwargs["total_issues"],
158 kwargs["total_fixed"],
159 kwargs["total_remaining"],
160 ),
161 )
163 import lintro.config.config_loader as config_loader
164 import lintro.utils.console as console_module
165 import lintro.utils.logger_setup as logger_setup
166 from lintro.utils.output import OutputManager
168 monkeypatch.setattr(config_loader, "get_config", lambda: lintro_config)
169 monkeypatch.setattr(
170 console_module,
171 "create_logger",
172 lambda **kwargs: fake_logger,
173 )
174 monkeypatch.setattr(
175 logger_setup,
176 "setup_execution_logging",
177 lambda run_dir, debug=False: None,
178 )
179 monkeypatch.setattr(
180 OutputManager,
181 "write_reports_from_results",
182 lambda self, results: None,
183 )
184 monkeypatch.setattr(te, "load_post_checks_config", lambda: {"enabled": False})
186 def _fake_ai_enhancement(**kwargs):
187 result = kwargs["all_results"][0]
188 result.success = True
189 result.fixed_issues_count = result.issues_count
190 result.remaining_issues_count = 0
191 result.issues_count = 0
193 import lintro.ai.hook as hook_module
195 class _FakeHook:
196 def should_run(self, action: Any) -> bool:
197 return True
199 def execute(
200 self,
201 action: Any,
202 all_results: Any,
203 *,
204 console_logger: Any,
205 output_format: Any,
206 ) -> object:
207 _fake_ai_enhancement(all_results=all_results)
208 return object() # non-None sentinel to signal hook ran
210 monkeypatch.setattr(
211 hook_module,
212 "AIPostExecutionHook",
213 lambda lintro_config, ai_fix=False: _FakeHook(),
214 )
216 captured: dict[str, int] = {}
218 def _capture_exit_code(
219 *,
220 action,
221 all_results,
222 total_issues,
223 total_remaining,
224 main_phase_empty_due_to_filter,
225 ):
226 captured["total_issues"] = total_issues
227 captured["total_remaining"] = total_remaining
228 return 0 if total_remaining == 0 else 1
230 monkeypatch.setattr(te, "determine_exit_code", _capture_exit_code)
232 exit_code = run_lint_tools_simple(
233 action="fmt",
234 paths=["."],
235 tools="ruff",
236 tool_options=None,
237 exclude=None,
238 include_venv=False,
239 group_by="auto",
240 output_format="json",
241 verbose=False,
242 raw_output=False,
243 )
245 assert_that(exit_code).is_equal_to(0)
246 assert_that(captured.get("total_issues")).is_equal_to(0)
247 assert_that(captured.get("total_remaining")).is_equal_to(0)