Coverage for tests / unit / utils / test_fix_retry.py: 100%
53 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 fix convergence retry logic."""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from typing import Any
8from assertpy import assert_that
10from lintro.models.core.tool_result import ToolResult
11from lintro.utils.tool_executor import _run_fix_with_retry
14@dataclass
15class _MockToolDefinition:
16 """Minimal mock tool definition.
18 Attributes:
19 name: Name of the tool.
20 """
22 name: str = "mock_tool"
25@dataclass
26class _ConvergingMockTool:
27 """Mock tool that converges after a given number of fix passes.
29 Attributes:
30 definition: Tool definition mock.
31 converge_on_pass: Pass number (1-based) on which remaining goes to 0.
32 """
34 definition: _MockToolDefinition = field(default_factory=_MockToolDefinition)
35 converge_on_pass: int = 2
36 _call_count: int = field(default=0, init=False)
38 def fix(
39 self,
40 paths: list[str],
41 options: dict[str, Any] | None = None,
42 ) -> ToolResult:
43 """Mock fix that converges after converge_on_pass calls.
45 Args:
46 paths: Paths to fix.
47 options: Fix options.
49 Returns:
50 ToolResult with remaining issues depending on call count.
51 """
52 self._call_count += 1
53 if self._call_count >= self.converge_on_pass:
54 return ToolResult(
55 name=self.definition.name,
56 success=True,
57 output="all fixed",
58 issues_count=0,
59 initial_issues_count=3,
60 fixed_issues_count=3,
61 remaining_issues_count=0,
62 )
63 return ToolResult(
64 name=self.definition.name,
65 success=False,
66 output="still has issues",
67 issues_count=1,
68 initial_issues_count=3,
69 fixed_issues_count=2,
70 remaining_issues_count=1,
71 )
74@dataclass
75class _NeverConvergingMockTool:
76 """Mock tool that never converges.
78 Attributes:
79 definition: Tool definition mock.
80 """
82 definition: _MockToolDefinition = field(default_factory=_MockToolDefinition)
83 _call_count: int = field(default=0, init=False)
85 def fix(
86 self,
87 paths: list[str],
88 options: dict[str, Any] | None = None,
89 ) -> ToolResult:
90 """Mock fix that always reports remaining issues.
92 Args:
93 paths: Paths to fix.
94 options: Fix options.
96 Returns:
97 ToolResult with remaining issues.
98 """
99 self._call_count += 1
100 return ToolResult(
101 name=self.definition.name,
102 success=False,
103 output="unfixable",
104 issues_count=2,
105 initial_issues_count=5,
106 fixed_issues_count=3,
107 remaining_issues_count=2,
108 )
111def test_fix_converges_on_second_attempt() -> None:
112 """Should succeed when tool converges on the second pass."""
113 tool = _ConvergingMockTool(converge_on_pass=2)
115 result = _run_fix_with_retry(
116 tool=tool, # type: ignore[arg-type]
117 paths=["."],
118 options={},
119 max_retries=3,
120 )
122 assert_that(result.success).is_true()
123 assert_that(result.remaining_issues_count).is_equal_to(0)
124 assert_that(result.initial_issues_count).is_equal_to(3)
125 assert_that(result.fixed_issues_count).is_equal_to(3)
126 assert_that(tool._call_count).is_equal_to(2)
129def test_fix_reports_unfixable_after_max_retries() -> None:
130 """Should report remaining issues when max retries are exhausted."""
131 tool = _NeverConvergingMockTool()
133 result = _run_fix_with_retry(
134 tool=tool, # type: ignore[arg-type]
135 paths=["."],
136 options={},
137 max_retries=3,
138 )
140 assert_that(result.success).is_false()
141 assert_that(result.remaining_issues_count).is_equal_to(2)
142 assert_that(tool._call_count).is_equal_to(3)
145def test_fix_no_retry_when_first_pass_succeeds() -> None:
146 """Should not retry when the first pass succeeds."""
147 tool = _ConvergingMockTool(converge_on_pass=1)
149 result = _run_fix_with_retry(
150 tool=tool, # type: ignore[arg-type]
151 paths=["."],
152 options={},
153 max_retries=3,
154 )
156 assert_that(result.success).is_true()
157 assert_that(result.remaining_issues_count).is_equal_to(0)
158 assert_that(tool._call_count).is_equal_to(1)
161def test_fix_retry_merges_results_correctly() -> None:
162 """Should keep initial count from first pass and remaining from last."""
163 tool = _ConvergingMockTool(converge_on_pass=3)
165 result = _run_fix_with_retry(
166 tool=tool, # type: ignore[arg-type]
167 paths=["."],
168 options={},
169 max_retries=5,
170 )
172 # initial_issues_count should be from the first pass (3)
173 assert_that(result.initial_issues_count).is_equal_to(3)
174 # remaining from last pass (0 since it converged on pass 3)
175 assert_that(result.remaining_issues_count).is_equal_to(0)
176 # fixed = initial - remaining = 3 - 0 = 3
177 assert_that(result.fixed_issues_count).is_equal_to(3)
178 assert_that(tool._call_count).is_equal_to(3)