Coverage for tests / unit / ai / test_prompts.py: 100%
94 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 prompt templates."""
3from __future__ import annotations
5import re
7import pytest
8from assertpy import assert_that
10from lintro.ai.prompts import (
11 FIX_BATCH_PROMPT_TEMPLATE,
12 FIX_PROMPT_TEMPLATE,
13 FIX_SYSTEM,
14 POST_FIX_SUMMARY_PROMPT_TEMPLATE,
15 REFINEMENT_PROMPT_TEMPLATE,
16 SUMMARY_PROMPT_TEMPLATE,
17 SUMMARY_SYSTEM,
18)
20# Regex matching un-interpolated single-brace placeholders like {var} but not
21# escaped double-braces like {{ or }}.
22_LEFTOVER_PLACEHOLDER = re.compile(r"(?<!\{)\{[a-z_]+\}(?!\})")
25# ---------------------------------------------------------------------------
26# FIX_SYSTEM
27# ---------------------------------------------------------------------------
30def test_fix_system_is_non_empty():
31 """FIX_SYSTEM must be a non-empty string."""
32 assert_that(FIX_SYSTEM).is_not_empty()
35def test_fix_system_mentions_json():
36 """FIX_SYSTEM instructs the model to respond with JSON."""
37 assert_that(FIX_SYSTEM).contains("JSON")
40# ---------------------------------------------------------------------------
41# FIX_PROMPT_TEMPLATE — basic rendering
42# ---------------------------------------------------------------------------
44_FIX_DEFAULTS = {
45 "tool_name": "ruff",
46 "code": "E501",
47 "file": "main.py",
48 "line": 42,
49 "message": "Line too long",
50 "context_start": 37,
51 "context_end": 47,
52 "code_context": "x = 1",
53 "boundary": "CODE_BLOCK_test1234",
54}
57def test_prompts_template_renders():
58 """Verify the fix prompt template renders with all required placeholders."""
59 assert_that(FIX_SYSTEM).is_not_empty()
60 result = FIX_PROMPT_TEMPLATE.format(**_FIX_DEFAULTS)
61 assert_that(result).contains("ruff")
62 assert_that(result).contains("main.py")
65def test_prompts_template_includes_risk_level():
66 """Verify the fix prompt template contains a risk_level placeholder."""
67 assert_that(FIX_PROMPT_TEMPLATE).contains("risk_level")
70def test_fix_prompt_no_leftover_placeholders():
71 """All placeholders in FIX_PROMPT_TEMPLATE are interpolated."""
72 result = FIX_PROMPT_TEMPLATE.format(**_FIX_DEFAULTS)
73 assert_that(_LEFTOVER_PLACEHOLDER.findall(result)).is_empty()
76def test_fix_prompt_contains_all_values():
77 """Every supplied value appears verbatim in the rendered prompt."""
78 result = FIX_PROMPT_TEMPLATE.format(**_FIX_DEFAULTS)
79 for value in (
80 "ruff",
81 "E501",
82 "main.py",
83 "42",
84 "Line too long",
85 "37",
86 "47",
87 "x = 1",
88 ):
89 assert_that(result).contains(value)
92# ---------------------------------------------------------------------------
93# FIX_PROMPT_TEMPLATE — various issue types
94# ---------------------------------------------------------------------------
97@pytest.mark.parametrize(
98 ("tool_name", "code", "message"),
99 [
100 ("ruff", "E501", "Line too long (120 > 79 characters)"),
101 ("mypy", "attr-defined", "Module has no attribute 'foo'"),
102 ("pylint", "C0114", "Missing module docstring"),
103 ("flake8", "F401", "'os' imported but unused"),
104 ("eslint", "no-unused-vars", "'x' is defined but never used"),
105 ],
106)
107def test_fix_prompt_renders_various_issue_types(tool_name, code, message):
108 """FIX_PROMPT_TEMPLATE renders correctly for diverse tool/code combos."""
109 result = FIX_PROMPT_TEMPLATE.format(
110 tool_name=tool_name,
111 code=code,
112 file="src/app.py",
113 line=10,
114 message=message,
115 context_start=5,
116 context_end=15,
117 code_context="pass",
118 boundary="CODE_BLOCK_test1234",
119 )
120 assert_that(result).contains(tool_name)
121 assert_that(result).contains(code)
122 assert_that(result).contains(message)
123 assert_that(_LEFTOVER_PLACEHOLDER.findall(result)).is_empty()
126# ---------------------------------------------------------------------------
127# Special characters in messages and file paths
128# ---------------------------------------------------------------------------
131def test_fix_prompt_special_characters_in_message():
132 """Quotes, newlines, and backslashes in the message survive rendering."""
133 msg = "Expected \"int\" but got 'str'\nDetails: see C:\\path"
134 result = FIX_PROMPT_TEMPLATE.format(
135 **{**_FIX_DEFAULTS, "message": msg},
136 )
137 assert_that(result).contains('"int"')
138 assert_that(result).contains("'str'")
139 assert_that(result).contains("C:\\path")
142def test_fix_prompt_unicode_in_file_path():
143 """Unicode characters in file paths are preserved."""
144 path = "src/modulos/\u00e9l\u00e8ve.py"
145 result = FIX_PROMPT_TEMPLATE.format(
146 **{**_FIX_DEFAULTS, "file": path},
147 )
148 assert_that(result).contains(path)
151def test_fix_prompt_spaces_in_file_path():
152 """File paths containing spaces are preserved."""
153 path = "my project/src/hello world.py"
154 result = FIX_PROMPT_TEMPLATE.format(
155 **{**_FIX_DEFAULTS, "file": path},
156 )
157 assert_that(result).contains(path)
160# ---------------------------------------------------------------------------
161# Edge cases: empty code context, zero line, very long message
162# ---------------------------------------------------------------------------
165def test_fix_prompt_empty_code_context():
166 """Empty code_context still produces a valid rendered string."""
167 result = FIX_PROMPT_TEMPLATE.format(
168 **{**_FIX_DEFAULTS, "code_context": ""},
169 )
170 assert_that(result).is_not_empty()
171 assert_that(_LEFTOVER_PLACEHOLDER.findall(result)).is_empty()
174def test_fix_prompt_zero_line_number():
175 """Line number 0 renders without error."""
176 result = FIX_PROMPT_TEMPLATE.format(
177 **{**_FIX_DEFAULTS, "line": 0},
178 )
179 assert_that(result).contains("Line: 0")
180 assert_that(_LEFTOVER_PLACEHOLDER.findall(result)).is_empty()
183def test_fix_prompt_very_long_message():
184 """A very long message does not break rendering."""
185 long_msg = "A" * 10_000
186 result = FIX_PROMPT_TEMPLATE.format(
187 **{**_FIX_DEFAULTS, "message": long_msg},
188 )
189 assert_that(result).contains(long_msg)
192# ---------------------------------------------------------------------------
193# SUMMARY_PROMPT_TEMPLATE and SUMMARY_SYSTEM
194# ---------------------------------------------------------------------------
197def test_summary_system_is_non_empty():
198 """SUMMARY_SYSTEM must be a non-empty string."""
199 assert_that(SUMMARY_SYSTEM).is_not_empty()
202def test_summary_prompt_renders():
203 """SUMMARY_PROMPT_TEMPLATE renders with all required variables."""
204 result = SUMMARY_PROMPT_TEMPLATE.format(
205 total_issues=42,
206 tool_count=3,
207 issues_digest="ruff: E501 x 10",
208 )
209 assert_that(result).contains("42")
210 assert_that(result).contains("3")
211 assert_that(result).contains("ruff: E501 x 10")
214def test_summary_prompt_no_leftover_placeholders():
215 """All placeholders are interpolated in SUMMARY_PROMPT_TEMPLATE."""
216 result = SUMMARY_PROMPT_TEMPLATE.format(
217 total_issues=0,
218 tool_count=0,
219 issues_digest="",
220 )
221 assert_that(_LEFTOVER_PLACEHOLDER.findall(result)).is_empty()
224def test_summary_prompt_recommends_lintro():
225 """SUMMARY_PROMPT_TEMPLATE tells the model to recommend lintro commands."""
226 assert_that(SUMMARY_PROMPT_TEMPLATE).contains("lintro chk")
227 assert_that(SUMMARY_PROMPT_TEMPLATE).contains("lintro fmt")
230# ---------------------------------------------------------------------------
231# REFINEMENT_PROMPT_TEMPLATE
232# ---------------------------------------------------------------------------
235def test_refinement_prompt_renders():
236 """REFINEMENT_PROMPT_TEMPLATE renders with all required variables."""
237 result = REFINEMENT_PROMPT_TEMPLATE.format(
238 tool_name="ruff",
239 code="E501",
240 file="main.py",
241 line=10,
242 previous_suggestion="old fix",
243 new_error="still too long",
244 context_start=5,
245 context_end=15,
246 code_context="x = 1",
247 boundary="CODE_BLOCK_test1234",
248 )
249 assert_that(result).contains("old fix")
250 assert_that(result).contains("still too long")
251 assert_that(_LEFTOVER_PLACEHOLDER.findall(result)).is_empty()
254# ---------------------------------------------------------------------------
255# FIX_BATCH_PROMPT_TEMPLATE
256# ---------------------------------------------------------------------------
259def test_batch_prompt_renders():
260 """FIX_BATCH_PROMPT_TEMPLATE renders with all required variables."""
261 result = FIX_BATCH_PROMPT_TEMPLATE.format(
262 tool_name="ruff",
263 file="app.py",
264 issues_list="1. E501 line 10\n2. E302 line 20",
265 file_content="import os\n",
266 boundary="CODE_BLOCK_test1234",
267 )
268 assert_that(result).contains("ruff")
269 assert_that(result).contains("app.py")
270 assert_that(result).contains("E501 line 10")
271 assert_that(_LEFTOVER_PLACEHOLDER.findall(result)).is_empty()
274# ---------------------------------------------------------------------------
275# POST_FIX_SUMMARY_PROMPT_TEMPLATE
276# ---------------------------------------------------------------------------
279def test_post_fix_summary_prompt_renders():
280 """POST_FIX_SUMMARY_PROMPT_TEMPLATE renders with all required variables."""
281 result = POST_FIX_SUMMARY_PROMPT_TEMPLATE.format(
282 applied=5,
283 rejected=2,
284 remaining=3,
285 issues_digest="mypy: attr-defined x 3",
286 )
287 assert_that(result).contains("5")
288 assert_that(result).contains("2")
289 assert_that(result).contains("3")
290 assert_that(result).contains("mypy: attr-defined x 3")
293def test_post_fix_summary_prompt_no_leftover_placeholders():
294 """All placeholders are interpolated in POST_FIX_SUMMARY_PROMPT_TEMPLATE."""
295 result = POST_FIX_SUMMARY_PROMPT_TEMPLATE.format(
296 applied=0,
297 rejected=0,
298 remaining=0,
299 issues_digest="",
300 )
301 assert_that(_LEFTOVER_PLACEHOLDER.findall(result)).is_empty()
304def test_post_fix_summary_recommends_lintro():
305 """POST_FIX_SUMMARY_PROMPT_TEMPLATE tells the model to use lintro commands."""
306 assert_that(POST_FIX_SUMMARY_PROMPT_TEMPLATE).contains("lintro chk")
307 assert_that(POST_FIX_SUMMARY_PROMPT_TEMPLATE).contains("lintro fmt")