Coverage for tests / unit / ai / test_validation_core.py: 100%
93 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 core validation logic and rendering.
3Covers _validate_suggestions, verify_fixes, and render_validation_terminal.
4"""
6from __future__ import annotations
8from unittest.mock import MagicMock, patch
10from assertpy import assert_that
12from lintro.ai.display import render_validation_terminal
13from lintro.ai.models import AIFixSuggestion
14from lintro.ai.validation import (
15 ValidationResult,
16 _validate_suggestions,
17 verify_fixes,
18)
19from lintro.models.core.tool_result import ToolResult
20from lintro.parsers.base_issue import BaseIssue
22from .conftest import MockIssue
25def _make_suggestion(
26 *,
27 file: str = "src/main.py",
28 line: int = 10,
29 code: str = "B101",
30 tool_name: str = "ruff",
31) -> AIFixSuggestion:
32 """Create an AIFixSuggestion for testing."""
33 return AIFixSuggestion(
34 file=file,
35 line=line,
36 code=code,
37 tool_name=tool_name,
38 original_code="assert x",
39 suggested_code="if not x: raise",
40 explanation="Replace assert",
41 )
44# -- render_validation_terminal ------------------------------------------------
47def test_render_validation_terminal_renders_verified():
48 """Verify terminal rendering displays the verified fix count."""
49 result = ValidationResult(verified=3, unverified=0)
50 output = render_validation_terminal(result)
51 assert_that(output).contains("3 resolved")
54def test_render_validation_terminal_renders_unverified_with_details():
55 """Verify terminal rendering shows unverified count and detail lines."""
56 result = ValidationResult(
57 verified=1,
58 unverified=2,
59 details=[
60 "[B101] main.py:10 — issue still present",
61 "[E501] utils.py:25 — issue still present",
62 ],
63 )
64 output = render_validation_terminal(result)
65 assert_that(output).contains("1 resolved")
66 assert_that(output).contains("2 still present")
67 assert_that(output).contains("B101")
68 assert_that(output).contains("E501")
71def test_render_validation_terminal_empty_result_returns_empty():
72 """Verify an empty ValidationResult produces empty terminal output."""
73 result = ValidationResult()
74 output = render_validation_terminal(result)
75 assert_that(output).is_empty()
78# -- _validate_suggestions (shared core logic) --------------------------------
81def test_validate_suggestions_verified_when_no_remaining_issues():
82 """Core validation marks fix as verified when no issues remain for the tool."""
83 suggestion = _make_suggestion(tool_name="ruff", code="B101")
84 result = _validate_suggestions([suggestion], {"ruff": []})
86 assert_that(result.verified).is_equal_to(1)
87 assert_that(result.unverified).is_equal_to(0)
90def test_validate_suggestions_unverified_when_issue_remains():
91 """Core validation marks fix as unverified when the issue still appears."""
92 remaining = MagicMock()
93 remaining.file = "src/main.py"
94 remaining.code = "B101"
95 remaining.line = 10
97 suggestion = _make_suggestion(tool_name="ruff", code="B101", line=10)
98 result = _validate_suggestions([suggestion], {"ruff": [remaining]})
100 assert_that(result.verified).is_equal_to(0)
101 assert_that(result.unverified).is_equal_to(1)
104def test_validate_suggestions_skips_tool_without_fresh_issues():
105 """When a tool has no entry in fresh_issues_by_tool, its suggestions are skipped."""
106 suggestion = _make_suggestion(tool_name="ruff")
107 result = _validate_suggestions([suggestion], {})
109 assert_that(result.verified).is_equal_to(0)
110 assert_that(result.unverified).is_equal_to(0)
113# -- verify_fixes (unified entry point) ---------------------------------------
116def test_verify_fixes_returns_none_for_empty_suggestions():
117 """verify_fixes returns None when given an empty suggestions list."""
118 result = verify_fixes(applied_suggestions=[], by_tool={})
119 assert_that(result).is_none()
122@patch("lintro.ai.rerun.rerun_tools")
123@patch("lintro.ai.rerun.apply_rerun_results")
124def test_verify_fixes_runs_tools_once_and_validates(
125 mock_apply_rerun,
126 mock_rerun_tools,
127):
128 """verify_fixes calls rerun_tools once and produces a ValidationResult."""
129 # Set up a fresh rerun result with no remaining issues
130 fresh_result = ToolResult(
131 name="ruff",
132 success=True,
133 issues_count=0,
134 issues=[],
135 )
136 mock_rerun_tools.return_value = [fresh_result]
138 suggestion = _make_suggestion(tool_name="ruff")
139 issue = MockIssue(
140 file="src/main.py",
141 line=10,
142 column=1,
143 message="test",
144 code="B101",
145 severity="low",
146 )
147 original_result = ToolResult(name="ruff", success=False, issues_count=1)
148 issues: list[BaseIssue] = [issue]
149 by_tool = {"ruff": (original_result, issues)}
151 result = verify_fixes(
152 applied_suggestions=[suggestion],
153 by_tool=by_tool,
154 )
156 assert_that(result).is_not_none()
157 assert_that(result.verified).is_equal_to(1) # type: ignore[union-attr] # assertpy is_not_none narrows this
158 assert_that(result.unverified).is_equal_to(0) # type: ignore[union-attr] # assertpy is_not_none narrows this
159 # rerun_tools should be called exactly once (not twice as before)
160 mock_rerun_tools.assert_called_once_with(by_tool)
161 mock_apply_rerun.assert_called_once()
164@patch("lintro.ai.rerun.rerun_tools")
165@patch("lintro.ai.rerun.apply_rerun_results")
166def test_verify_fixes_updates_tool_results_and_validates(
167 mock_apply_rerun,
168 mock_rerun_tools,
169):
170 """verify_fixes both updates ToolResults (via apply_rerun_results) and validates."""
171 remaining = MagicMock()
172 remaining.file = "src/main.py"
173 remaining.code = "B101"
174 remaining.line = 10
176 fresh_result = ToolResult(
177 name="ruff",
178 success=True,
179 issues_count=1,
180 issues=[remaining],
181 )
182 mock_rerun_tools.return_value = [fresh_result]
184 suggestion = _make_suggestion(tool_name="ruff", code="B101", line=10)
185 issue = MockIssue(
186 file="src/main.py",
187 line=10,
188 column=1,
189 message="test",
190 code="B101",
191 severity="low",
192 )
193 original_result = ToolResult(name="ruff", success=False, issues_count=1)
194 issues: list[BaseIssue] = [issue]
195 by_tool = {"ruff": (original_result, issues)}
197 result = verify_fixes(
198 applied_suggestions=[suggestion],
199 by_tool=by_tool,
200 )
202 assert_that(result).is_not_none()
203 assert_that(result.unverified).is_equal_to(1) # type: ignore[union-attr] # assertpy is_not_none narrows this
204 assert_that(result.verified).is_equal_to(0) # type: ignore[union-attr] # assertpy is_not_none narrows this
205 # Confirms apply_rerun_results was called to update ToolResult objects
206 mock_apply_rerun.assert_called_once()
209@patch("lintro.ai.rerun.rerun_tools")
210def test_verify_fixes_handles_no_rerun_results(mock_rerun_tools):
211 """verify_fixes handles the case where rerun_tools returns None."""
212 mock_rerun_tools.return_value = None
214 suggestion = _make_suggestion(tool_name="ruff")
215 issue = MockIssue(
216 file="src/main.py",
217 line=10,
218 column=1,
219 message="test",
220 code="B101",
221 severity="low",
222 )
223 original_result = ToolResult(name="ruff", success=False, issues_count=1)
224 issues: list[BaseIssue] = [issue]
225 by_tool = {"ruff": (original_result, issues)}
227 result = verify_fixes(
228 applied_suggestions=[suggestion],
229 by_tool=by_tool,
230 )
232 # With no rerun results, verify_fixes returns None
233 assert_that(result).is_none()