Python · 6958 bytes Raw Blame History
1 """Tests for the error recovery system."""
2
3 from loader.runtime.recovery import (
4 ErrorCategory,
5 RecoveryContext,
6 categorize_error,
7 format_failure_message,
8 format_recovery_prompt,
9 get_recovery_hints,
10 )
11
12
13 class TestCategorizeError:
14 """Tests for error categorization."""
15
16 def test_file_not_found(self):
17 assert categorize_error("No such file or directory") == ErrorCategory.FILE_NOT_FOUND
18 assert categorize_error("file not found: test.py") == ErrorCategory.FILE_NOT_FOUND
19 assert categorize_error("Path does not exist") == ErrorCategory.FILE_NOT_FOUND
20
21 def test_permission_denied(self):
22 assert categorize_error("Permission denied") == ErrorCategory.PERMISSION_DENIED
23 assert categorize_error("Access denied to file") == ErrorCategory.PERMISSION_DENIED
24 assert categorize_error("Operation not permitted") == ErrorCategory.PERMISSION_DENIED
25
26 def test_syntax_error(self):
27 assert categorize_error("SyntaxError: invalid syntax") == ErrorCategory.SYNTAX_ERROR
28 assert categorize_error("Parse error at line 5") == ErrorCategory.SYNTAX_ERROR
29
30 def test_command_not_found(self):
31 assert categorize_error("command not found: foo") == ErrorCategory.COMMAND_NOT_FOUND
32 assert categorize_error("'bar' is not recognized") == ErrorCategory.COMMAND_NOT_FOUND
33
34 def test_timeout(self):
35 assert categorize_error("Operation timed out") == ErrorCategory.TIMEOUT
36 assert categorize_error("Connection timeout") == ErrorCategory.TIMEOUT
37
38 def test_invalid_arguments(self):
39 assert categorize_error("Invalid argument: path") == ErrorCategory.INVALID_ARGUMENTS
40 assert categorize_error("Missing required parameter") == ErrorCategory.INVALID_ARGUMENTS
41
42 def test_network_error(self):
43 assert categorize_error("Network unreachable") == ErrorCategory.NETWORK_ERROR
44 assert categorize_error("Connection refused") == ErrorCategory.CONNECTION_REFUSED
45
46 def test_unknown(self):
47 assert categorize_error("Something weird happened") == ErrorCategory.UNKNOWN
48 assert categorize_error("") == ErrorCategory.UNKNOWN
49
50
51 class TestRecoveryContext:
52 """Tests for RecoveryContext tracking."""
53
54 def test_add_attempt(self):
55 ctx = RecoveryContext(
56 original_tool="read",
57 original_args={"path": "test.py"},
58 )
59 assert len(ctx.attempts) == 0
60
61 ctx.add_attempt("read", {"path": "test.py"}, "File not found")
62 assert len(ctx.attempts) == 1
63 assert ctx.attempts[0].tool_name == "read"
64 assert ctx.attempts[0].category == ErrorCategory.FILE_NOT_FOUND
65
66 def test_can_retry(self):
67 ctx = RecoveryContext(
68 original_tool="read",
69 original_args={"path": "test.py"},
70 max_retries=3,
71 )
72 assert ctx.can_retry()
73
74 ctx.add_attempt("read", {"path": "test.py"}, "Error 1")
75 assert ctx.can_retry()
76
77 ctx.add_attempt("read", {"path": "test2.py"}, "Error 2")
78 assert ctx.can_retry()
79
80 ctx.add_attempt("read", {"path": "test3.py"}, "Error 3")
81 assert not ctx.can_retry()
82
83 def test_was_tried(self):
84 ctx = RecoveryContext(
85 original_tool="read",
86 original_args={"path": "test.py"},
87 )
88 assert not ctx.was_tried("read", {"path": "test.py"})
89
90 ctx.add_attempt("read", {"path": "test.py"}, "Error")
91 assert ctx.was_tried("read", {"path": "test.py"})
92 assert not ctx.was_tried("read", {"path": "other.py"})
93 assert not ctx.was_tried("write", {"path": "test.py"})
94
95 def test_attempts_summary(self):
96 ctx = RecoveryContext(
97 original_tool="read",
98 original_args={"path": "test.py"},
99 )
100 assert ctx.attempts_summary() == ""
101
102 ctx.add_attempt("read", {"path": "test.py"}, "File not found")
103 summary = ctx.attempts_summary()
104 assert "Previous attempts:" in summary
105 assert "read" in summary
106 assert "File not found" in summary
107
108
109 class TestGetRecoveryHints:
110 """Tests for recovery hints."""
111
112 def test_file_not_found_hints(self):
113 hints = get_recovery_hints(ErrorCategory.FILE_NOT_FOUND, "read")
114 assert "glob" in hints.lower()
115 assert "directory" in hints.lower()
116
117 def test_edit_file_not_found_special_hint(self):
118 hints = get_recovery_hints(ErrorCategory.FILE_NOT_FOUND, "edit")
119 assert "write" in hints.lower()
120
121 def test_bash_command_not_found_special_hint(self):
122 hints = get_recovery_hints(ErrorCategory.COMMAND_NOT_FOUND, "bash")
123 assert "which" in hints.lower()
124
125 def test_bash_text_rewrite_hint_prefers_file_tools(self):
126 hints = get_recovery_hints(
127 ErrorCategory.UNKNOWN,
128 "bash",
129 {"command": "sed -i '1,3c\\updated' index.html"},
130 )
131 assert "edit/patch/write" in hints.lower()
132 assert "index.html" in hints
133
134
135 class TestFormatRecoveryPrompt:
136 """Tests for recovery prompt formatting."""
137
138 def test_format_recovery_prompt(self):
139 ctx = RecoveryContext(
140 original_tool="read",
141 original_args={"path": "test.py"},
142 )
143 ctx.add_attempt("read", {"path": "test.py"}, "No such file")
144
145 prompt = format_recovery_prompt(ctx, "read", {"path": "test.py"}, "No such file")
146 assert "Failed Command" in prompt
147 assert "read(path='test.py')" in prompt
148 assert "No such file" in prompt
149 assert "1/3" in prompt
150 assert "retry the same command with slight variations" in prompt
151
152 def test_format_recovery_prompt_for_failed_shell_rewrite_points_to_file_tools(self):
153 ctx = RecoveryContext(
154 original_tool="bash",
155 original_args={"command": "sed -i '1,3c\\updated' index.html"},
156 )
157 ctx.add_attempt(
158 "bash",
159 {"command": "sed -i '1,3c\\updated' index.html"},
160 "Exit code 1",
161 )
162
163 prompt = format_recovery_prompt(
164 ctx,
165 "bash",
166 {"command": "sed -i '1,3c\\updated' index.html"},
167 "Exit code 1",
168 )
169
170 assert "edit/patch/write" in prompt.lower()
171 assert "index.html" in prompt
172
173
174 class TestFormatFailureMessage:
175 """Tests for failure message formatting."""
176
177 def test_format_failure_message(self):
178 ctx = RecoveryContext(
179 original_tool="read",
180 original_args={"path": "test.py"},
181 )
182 ctx.add_attempt("read", {"path": "test.py"}, "Error 1")
183 ctx.add_attempt("glob", {"pattern": "*.py"}, "Error 2")
184 ctx.add_attempt("read", {"path": "src/test.py"}, "Error 3")
185
186 msg = format_failure_message(ctx)
187 assert "3 attempts" in msg
188 assert "read" in msg
189 assert "glob" in msg
190 assert "Error 1" in msg
191 assert "Error 2" in msg
192 assert "Error 3" in msg