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

1"""Tests for _build_fix_context. 

2 

3Covers full file context for small files, threshold, and token budget. 

4""" 

5 

6from __future__ import annotations 

7 

8import threading 

9 

10from assertpy import assert_that 

11 

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 

19 

20# --------------------------------------------------------------------------- 

21# P3-3: Full file context for small files 

22# --------------------------------------------------------------------------- 

23 

24 

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") 

29 

30 issue = MockIssue( 

31 file=str(source), 

32 line=2, 

33 code="E501", 

34 message="Line too long", 

35 ) 

36 

37 provider = MockAIProvider() 

38 generate_fixes( 

39 [issue], 

40 provider, 

41 tool_name="ruff", 

42 workspace_root=tmp_path, 

43 ) 

44 

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") 

51 

52 

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") 

58 

59 issue = MockIssue( 

60 file=str(source), 

61 line=25, 

62 code="E501", 

63 message="Line too long", 

64 ) 

65 

66 provider = MockAIProvider() 

67 retrying_call = with_retry(max_retries=0)(_call_provider) 

68 

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 ) 

80 

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") 

87 

88 

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") 

96 

97 issue = MockIssue( 

98 file=str(source), 

99 line=10, 

100 code="E501", 

101 message="Line too long", 

102 ) 

103 

104 provider = MockAIProvider() 

105 retrying_call = with_retry(max_retries=0)(_call_provider) 

106 

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 ) 

118 

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")