Coverage for tests / unit / ai / test_fix_context.py: 100%
40 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 _build_fix_context.
3Covers full file context for small files, threshold, and token budget.
4"""
6from __future__ import annotations
8import threading
10from assertpy import assert_that
12from lintro.ai.fix import (
13 _call_provider,
14 _generate_single_fix,
15 generate_fixes,
16)
17from lintro.ai.retry import with_retry
18from tests.unit.ai.conftest import MockAIProvider, MockIssue
20# ---------------------------------------------------------------------------
21# P3-3: Full file context for small files
22# ---------------------------------------------------------------------------
25def test_full_file_context_for_small_file(tmp_path):
26 """Small files should send full content as context (lines 1-N)."""
27 source = tmp_path / "small.py"
28 source.write_text("x = 1\ny = 2\nz = 3\n")
30 issue = MockIssue(
31 file=str(source),
32 line=2,
33 code="E501",
34 message="Line too long",
35 )
37 provider = MockAIProvider()
38 generate_fixes(
39 [issue],
40 provider,
41 tool_name="ruff",
42 workspace_root=tmp_path,
43 )
45 assert_that(provider.calls).is_length(1)
46 prompt = provider.calls[0]["prompt"]
47 # Full file sent: context window should span the entire file
48 assert_that(prompt).contains("lines 1-3")
49 assert_that(prompt).contains("x = 1")
50 assert_that(prompt).contains("z = 3")
53def test_full_file_skipped_when_file_exceeds_threshold(tmp_path):
54 """Files over full_file_threshold should use windowed context."""
55 # Create a file with 50 lines but set threshold to 5
56 source = tmp_path / "big.py"
57 source.write_text("\n".join(f"line_{i}" for i in range(1, 51)) + "\n")
59 issue = MockIssue(
60 file=str(source),
61 line=25,
62 code="E501",
63 message="Line too long",
64 )
66 provider = MockAIProvider()
67 retrying_call = with_retry(max_retries=0)(_call_provider)
69 _generate_single_fix(
70 issue,
71 provider,
72 "ruff",
73 {},
74 threading.Lock(),
75 tmp_path,
76 2048,
77 retrying_call,
78 full_file_threshold=5, # File has 50 lines, above threshold
79 )
81 assert_that(provider.calls).is_length(1)
82 prompt = provider.calls[0]["prompt"]
83 # Should NOT contain "lines 1-50" (full file); uses windowed context
84 assert_that(prompt).does_not_contain("lines 1-50")
85 # Should contain a windowed range around line 25
86 assert_that(prompt).contains("line_25")
89def test_full_file_skipped_when_over_token_budget(tmp_path):
90 """Full file that exceeds token budget should fall back to windowed context."""
91 # Create a file with 20 lines, set a tight token budget so full-file
92 # context is rejected and windowed context is used instead.
93 source = tmp_path / "medium.py"
94 lines = [f"line_{i} = {i}" for i in range(1, 21)]
95 source.write_text("\n".join(lines) + "\n")
97 issue = MockIssue(
98 file=str(source),
99 line=10,
100 code="E501",
101 message="Line too long",
102 )
104 provider = MockAIProvider()
105 retrying_call = with_retry(max_retries=0)(_call_provider)
107 _generate_single_fix(
108 issue,
109 provider,
110 "ruff",
111 {},
112 threading.Lock(),
113 tmp_path,
114 2048,
115 retrying_call,
116 max_prompt_tokens=10, # Very tight budget, full file won't fit
117 )
119 assert_that(provider.calls).is_length(1)
120 prompt = provider.calls[0]["prompt"]
121 # Should use windowed context, not full file (1-20)
122 assert_that(prompt).does_not_contain("lines 1-20")
123 # Should contain the target line
124 assert_that(prompt).contains("line_10")