"""Tests for the error recovery system.""" from loader.runtime.recovery import ( ErrorCategory, RecoveryContext, categorize_error, format_failure_message, format_recovery_prompt, get_recovery_hints, ) class TestCategorizeError: """Tests for error categorization.""" def test_file_not_found(self): assert categorize_error("No such file or directory") == ErrorCategory.FILE_NOT_FOUND assert categorize_error("file not found: test.py") == ErrorCategory.FILE_NOT_FOUND assert categorize_error("Path does not exist") == ErrorCategory.FILE_NOT_FOUND def test_permission_denied(self): assert categorize_error("Permission denied") == ErrorCategory.PERMISSION_DENIED assert categorize_error("Access denied to file") == ErrorCategory.PERMISSION_DENIED assert categorize_error("Operation not permitted") == ErrorCategory.PERMISSION_DENIED def test_syntax_error(self): assert categorize_error("SyntaxError: invalid syntax") == ErrorCategory.SYNTAX_ERROR assert categorize_error("Parse error at line 5") == ErrorCategory.SYNTAX_ERROR def test_command_not_found(self): assert categorize_error("command not found: foo") == ErrorCategory.COMMAND_NOT_FOUND assert categorize_error("'bar' is not recognized") == ErrorCategory.COMMAND_NOT_FOUND def test_timeout(self): assert categorize_error("Operation timed out") == ErrorCategory.TIMEOUT assert categorize_error("Connection timeout") == ErrorCategory.TIMEOUT def test_invalid_arguments(self): assert categorize_error("Invalid argument: path") == ErrorCategory.INVALID_ARGUMENTS assert categorize_error("Missing required parameter") == ErrorCategory.INVALID_ARGUMENTS assert ( categorize_error( "WriteTool.execute() missing 1 required positional argument: 'content'" ) == ErrorCategory.INVALID_ARGUMENTS ) assert ( categorize_error( "Error patching file: structured patch hunk consumed a different " "number of original lines than declared" ) == ErrorCategory.INVALID_ARGUMENTS ) def test_network_error(self): assert categorize_error("Network unreachable") == ErrorCategory.NETWORK_ERROR assert categorize_error("Connection refused") == ErrorCategory.CONNECTION_REFUSED def test_unknown(self): assert categorize_error("Something weird happened") == ErrorCategory.UNKNOWN assert categorize_error("") == ErrorCategory.UNKNOWN class TestRecoveryContext: """Tests for RecoveryContext tracking.""" def test_add_attempt(self): ctx = RecoveryContext( original_tool="read", original_args={"path": "test.py"}, ) assert len(ctx.attempts) == 0 ctx.add_attempt("read", {"path": "test.py"}, "File not found") assert len(ctx.attempts) == 1 assert ctx.attempts[0].tool_name == "read" assert ctx.attempts[0].category == ErrorCategory.FILE_NOT_FOUND def test_can_retry(self): ctx = RecoveryContext( original_tool="read", original_args={"path": "test.py"}, max_retries=3, ) assert ctx.can_retry() ctx.add_attempt("read", {"path": "test.py"}, "Error 1") assert ctx.can_retry() ctx.add_attempt("read", {"path": "test2.py"}, "Error 2") assert ctx.can_retry() ctx.add_attempt("read", {"path": "test3.py"}, "Error 3") assert not ctx.can_retry() def test_was_tried(self): ctx = RecoveryContext( original_tool="read", original_args={"path": "test.py"}, ) assert not ctx.was_tried("read", {"path": "test.py"}) ctx.add_attempt("read", {"path": "test.py"}, "Error") assert ctx.was_tried("read", {"path": "test.py"}) assert not ctx.was_tried("read", {"path": "other.py"}) assert not ctx.was_tried("write", {"path": "test.py"}) def test_attempts_summary(self): ctx = RecoveryContext( original_tool="read", original_args={"path": "test.py"}, ) assert ctx.attempts_summary() == "" ctx.add_attempt("read", {"path": "test.py"}, "File not found") summary = ctx.attempts_summary() assert "Previous attempts:" in summary assert "read" in summary assert "File not found" in summary class TestGetRecoveryHints: """Tests for recovery hints.""" def test_file_not_found_hints(self): hints = get_recovery_hints(ErrorCategory.FILE_NOT_FOUND, "read") assert "glob" in hints.lower() assert "directory" in hints.lower() def test_edit_file_not_found_special_hint(self): hints = get_recovery_hints(ErrorCategory.FILE_NOT_FOUND, "edit") assert "write" in hints.lower() def test_bash_command_not_found_special_hint(self): hints = get_recovery_hints(ErrorCategory.COMMAND_NOT_FOUND, "bash") assert "which" in hints.lower() def test_bash_text_rewrite_hint_prefers_file_tools(self): hints = get_recovery_hints( ErrorCategory.UNKNOWN, "bash", {"command": "sed -i '1,3c\\updated' index.html"}, ) assert "edit/patch/write" in hints.lower() assert "index.html" in hints def test_write_metadata_only_hint_requests_real_content_payload(self): hints = get_recovery_hints( ErrorCategory.INVALID_ARGUMENTS, "write", { "file_path": "~/Loader/guides/nginx/index.html", "content_chars": 1354, "content_lines": 30, }, ) assert "content='...'" in hints assert "content_chars" in hints assert "index.html" in hints class TestFormatRecoveryPrompt: """Tests for recovery prompt formatting.""" def test_format_recovery_prompt(self): ctx = RecoveryContext( original_tool="read", original_args={"path": "test.py"}, ) ctx.add_attempt("read", {"path": "test.py"}, "No such file") prompt = format_recovery_prompt(ctx, "read", {"path": "test.py"}, "No such file") assert "Failed Command" in prompt assert "read(path='test.py')" in prompt assert "No such file" in prompt assert "1/3" in prompt assert "retry the same command with slight variations" in prompt def test_format_recovery_prompt_for_failed_shell_rewrite_points_to_file_tools(self): ctx = RecoveryContext( original_tool="bash", original_args={"command": "sed -i '1,3c\\updated' index.html"}, ) ctx.add_attempt( "bash", {"command": "sed -i '1,3c\\updated' index.html"}, "Exit code 1", ) prompt = format_recovery_prompt( ctx, "bash", {"command": "sed -i '1,3c\\updated' index.html"}, "Exit code 1", ) assert "edit/patch/write" in prompt.lower() assert "index.html" in prompt def test_format_recovery_prompt_for_metadata_only_write_requests_real_payload(self): ctx = RecoveryContext( original_tool="write", original_args={ "file_path": "~/Loader/guides/nginx/index.html", "content_chars": 1354, "content_lines": 30, }, ) ctx.add_attempt( "write", { "file_path": "~/Loader/guides/nginx/index.html", "content_chars": 1354, "content_lines": 30, }, "WriteTool.execute() missing 1 required positional argument: 'content'", ) prompt = format_recovery_prompt( ctx, "write", { "file_path": "~/Loader/guides/nginx/index.html", "content_chars": 1354, "content_lines": 30, }, "WriteTool.execute() missing 1 required positional argument: 'content'", ) assert "content='...'" in prompt assert "content_chars" in prompt assert "index.html" in prompt def test_format_recovery_prompt_for_old_string_miss_prefers_current_text(self): ctx = RecoveryContext( original_tool="edit", original_args={ "file_path": "~/Loader/guides/nginx/chapters/02-installation.html", "old_string": "
Expanded.
", }, ) ctx.add_attempt( "edit", ctx.original_args, "old_string not found in file. Make sure it matches exactly.", ) prompt = format_recovery_prompt( ctx, "edit", ctx.original_args, "old_string not found in file. Make sure it matches exactly.", ) assert "`old_string` is stale" in prompt assert "current on-disk file" in prompt assert "write` with the complete replacement content" in prompt def test_format_recovery_prompt_for_patch_context_miss_prefers_replacement(self): ctx = RecoveryContext( original_tool="patch", original_args={ "file_path": "~/Loader/guides/nginx/chapters/05-load-balancing.html", "hunks": [ { "old_start": 64, "old_lines": 1, "lines": ["