Coverage for tests / unit / ai / test_config.py: 100%
74 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 configuration."""
3from __future__ import annotations
5import pytest
6from assertpy import assert_that
7from pydantic import ValidationError
9from lintro.ai.config import AIConfig
11# -- Defaults --------------------------------------------------------------
14def test_default_config_booleans_and_provider() -> None:
15 """All boolean defaults and provider are correct out of the box."""
16 config = AIConfig()
17 assert_that(config.enabled).is_false()
18 assert_that(config.provider).is_equal_to("anthropic")
19 assert_that(config.default_fix).is_false()
20 assert_that(config.auto_apply).is_false()
21 assert_that(config.auto_apply_safe_fixes).is_true()
22 assert_that(config.validate_after_group).is_false()
23 assert_that(config.show_cost_estimate).is_true()
26def test_default_config_optional_fields() -> None:
27 """Optional fields default to None."""
28 config = AIConfig()
29 assert_that(config.model).is_none()
30 assert_that(config.api_key_env).is_none()
33def test_default_config_numeric_fields() -> None:
34 """All numeric fields have correct defaults."""
35 config = AIConfig()
36 assert_that(config.max_tokens).is_equal_to(4096)
37 assert_that(config.max_fix_attempts).is_equal_to(20)
38 assert_that(config.max_parallel_calls).is_equal_to(5)
39 assert_that(config.max_retries).is_equal_to(2)
40 assert_that(config.api_timeout).is_equal_to(60.0)
41 assert_that(config.context_lines).is_equal_to(15)
42 assert_that(config.fix_search_radius).is_equal_to(5)
43 assert_that(config.retry_base_delay).is_equal_to(1.0)
44 assert_that(config.retry_max_delay).is_equal_to(30.0)
45 assert_that(config.retry_backoff_factor).is_equal_to(2.0)
48# -- Provider constraint ---------------------------------------------------
51def test_provider_anthropic() -> None:
52 """Provider 'anthropic' is accepted (Pydantic coerces str to AIProvider)."""
53 config = AIConfig(provider="anthropic") # type: ignore[arg-type] # tests YAML-style str coercion
54 assert_that(config.provider).is_equal_to("anthropic")
57def test_provider_openai() -> None:
58 """Provider 'openai' is accepted (Pydantic coerces str to AIProvider)."""
59 config = AIConfig(provider="openai") # type: ignore[arg-type] # tests YAML-style str coercion
60 assert_that(config.provider).is_equal_to("openai")
63def test_provider_invalid_rejected() -> None:
64 """An invalid provider string is rejected by validation."""
65 with pytest.raises(ValidationError):
66 AIConfig(provider="gemini") # type: ignore[arg-type] # intentionally invalid
69# -- Boolean overrides -----------------------------------------------------
72def test_enabled_override() -> None:
73 """Enabled can be set to True."""
74 config = AIConfig(enabled=True)
75 assert_that(config.enabled).is_true()
78def test_auto_apply_override() -> None:
79 """auto_apply can be toggled."""
80 config = AIConfig(auto_apply=True)
81 assert_that(config.auto_apply).is_true()
84def test_auto_apply_safe_fixes_override() -> None:
85 """auto_apply_safe_fixes can be disabled."""
86 config = AIConfig(auto_apply_safe_fixes=False)
87 assert_that(config.auto_apply_safe_fixes).is_false()
90def test_default_fix_override() -> None:
91 """default_fix can be enabled."""
92 config = AIConfig(default_fix=True)
93 assert_that(config.default_fix).is_true()
96def test_validate_after_group_override() -> None:
97 """validate_after_group can be enabled."""
98 config = AIConfig(validate_after_group=True)
99 assert_that(config.validate_after_group).is_true()
102# -- String fields ---------------------------------------------------------
105def test_custom_model() -> None:
106 """Custom model string is stored correctly."""
107 config = AIConfig(model="gpt-4-turbo")
108 assert_that(config.model).is_equal_to("gpt-4-turbo")
111def test_custom_api_key_env() -> None:
112 """Custom api_key_env is stored correctly."""
113 config = AIConfig(api_key_env="MY_API_KEY")
114 assert_that(config.api_key_env).is_equal_to("MY_API_KEY")
117# -- Numeric field boundary values (parametrized) --------------------------
120@pytest.mark.parametrize(
121 ("field_name", "valid_value", "expected"),
122 [
123 ("max_tokens", 1, 1),
124 ("max_tokens", 128000, 128000),
125 ("max_parallel_calls", 1, 1),
126 ("max_parallel_calls", 20, 20),
127 ("max_retries", 0, 0),
128 ("max_retries", 10, 10),
129 ("api_timeout", 1.0, 1.0),
130 ("api_timeout", 300.0, 300.0),
131 ("context_lines", 1, 1),
132 ("context_lines", 100, 100),
133 ("fix_search_radius", 1, 1),
134 ("fix_search_radius", 50, 50),
135 ("retry_base_delay", 0.1, 0.1),
136 ("retry_max_delay", 1.0, 1.0),
137 ("retry_backoff_factor", 1.0, 1.0),
138 ],
139 ids=[
140 "max_tokens=1",
141 "max_tokens=128000",
142 "max_parallel_calls=1",
143 "max_parallel_calls=20",
144 "max_retries=0",
145 "max_retries=10",
146 "api_timeout=1.0",
147 "api_timeout=300.0",
148 "context_lines=1",
149 "context_lines=100",
150 "fix_search_radius=1",
151 "fix_search_radius=50",
152 "retry_base_delay=0.1",
153 "retry_max_delay=1.0",
154 "retry_backoff_factor=1.0",
155 ],
156)
157def test_numeric_field_accepts_valid_value(
158 field_name: str,
159 valid_value: int | float,
160 expected: int | float,
161) -> None:
162 """Numeric field {field_name} accepts value {valid_value}."""
163 config = AIConfig(**{field_name: valid_value}) # type: ignore[arg-type] # dynamic field name
164 assert_that(getattr(config, field_name)).is_equal_to(expected)
167@pytest.mark.parametrize(
168 ("field_name", "invalid_value"),
169 [
170 ("max_tokens", 0),
171 ("max_parallel_calls", 0),
172 ("max_parallel_calls", 21),
173 ("max_retries", -1),
174 ("max_retries", 11),
175 ("api_timeout", 0.5),
176 ("context_lines", 0),
177 ("context_lines", 101),
178 ("fix_search_radius", 0),
179 ("fix_search_radius", 51),
180 ("retry_base_delay", 0.05),
181 ("retry_max_delay", 0.5),
182 ("retry_backoff_factor", 0.5),
183 ],
184 ids=[
185 "max_tokens=0",
186 "max_parallel_calls=0",
187 "max_parallel_calls=21",
188 "max_retries=-1",
189 "max_retries=11",
190 "api_timeout=0.5",
191 "context_lines=0",
192 "context_lines=101",
193 "fix_search_radius=0",
194 "fix_search_radius=51",
195 "retry_base_delay=0.05",
196 "retry_max_delay=0.5",
197 "retry_backoff_factor=0.5",
198 ],
199)
200def test_numeric_field_rejects_invalid_value(
201 field_name: str,
202 invalid_value: int | float,
203) -> None:
204 """Numeric field {field_name} rejects invalid value {invalid_value}."""
205 with pytest.raises(ValidationError):
206 AIConfig(**{field_name: invalid_value}) # type: ignore[arg-type] # dynamic field name
209# -- Cross-field validators ------------------------------------------------
212def test_retry_max_delay_less_than_base_raises() -> None:
213 """retry_max_delay < retry_base_delay raises ValidationError."""
214 with pytest.raises(ValidationError, match="retry_max_delay"):
215 AIConfig(retry_base_delay=5.0, retry_max_delay=1.0)
218# -- Extra fields forbidden ------------------------------------------------
221def test_extra_fields_forbidden() -> None:
222 """Unknown fields are rejected by the model."""
223 with pytest.raises(ValidationError):
224 AIConfig(unknown_field="value") # type: ignore[call-arg] # intentionally invalid