@@ -683,6 +683,172 @@ def test_fzr_success_no_error_in_info(self, tmp_path):
683683 assert "Error:" not in history_content
684684
685685
686+ # ===========================================================================
687+ # 1c. Windows-specific mock-based negative tests
688+ # ===========================================================================
689+
690+ class TestWindowsShErrorReporting :
691+ """Mock-based tests simulating Windows failure modes.
692+
693+ These tests use unittest.mock to simulate Windows-specific scenarios
694+ (missing bash, line endings, chmod no-op, WinError codes) without
695+ needing an actual Windows machine. They verify correct classification
696+ AND that wrong labels are NOT applied.
697+ """
698+
699+ # --- A. Missing bash on Windows ---
700+
701+ def test_windows_no_bash_reports_helpful_error (self ):
702+ """WinError 2 stderr should mention MSYS2 or FZ_SHELL_PATH."""
703+ msg = _classify_sh_error (
704+ "[WinError 2] The system cannot find the file specified" ,
705+ 1 , "script.sh" ,
706+ )
707+ assert msg is not None
708+ assert "msys2" in msg .lower () or "fz_shell_path" in msg .lower ()
709+
710+ def test_windows_no_bash_does_not_say_permission (self ):
711+ """Missing bash should NOT be classified as permission denied."""
712+ msg = _classify_sh_error (
713+ "[WinError 2] The system cannot find the file specified" ,
714+ 1 , "script.sh" ,
715+ )
716+ assert msg is not None
717+ assert "permission denied" not in msg .lower ()
718+
719+ def test_windows_no_bash_does_not_say_remote (self ):
720+ """Missing bash on Windows should NOT say remote."""
721+ msg = classify_error (
722+ "[WinError 2] The system cannot find the file specified" ,
723+ exit_code = 1 , command = "script.sh" , protocol = "sh" ,
724+ )
725+ assert "remote" not in msg .lower ()
726+
727+ # --- B. Missing commands in PATH (MSYS2 installed but incomplete) ---
728+
729+ def test_windows_grep_not_found_reports_command_not_found (self ):
730+ """'grep' not recognized on Windows should say 'command not found locally'."""
731+ msg = classify_error (
732+ "'grep' is not recognized as an internal or external command" ,
733+ exit_code = 1 , command = "grep pattern file.txt" , protocol = "sh" ,
734+ )
735+ assert "command not found" in msg .lower () or "not found locally" in msg .lower ()
736+
737+ def test_windows_awk_not_found_reports_command_not_found (self ):
738+ """'awk' not recognized on Windows should say 'command not found locally'."""
739+ msg = classify_error (
740+ "'awk' is not recognized as an internal or external command" ,
741+ exit_code = 1 , command = "awk '{print $1}' file.txt" , protocol = "sh" ,
742+ )
743+ assert "command not found" in msg .lower () or "not found locally" in msg .lower ()
744+
745+ def test_windows_missing_command_does_not_say_line_ending (self ):
746+ """Missing command should NOT be classified as line ending error."""
747+ msg = classify_error (
748+ "'grep' is not recognized as an internal or external command" ,
749+ exit_code = 1 , command = "grep pattern file.txt" , protocol = "sh" ,
750+ )
751+ assert "line ending" not in msg .lower ()
752+
753+ # --- C. Line endings (scripts created on Windows) ---
754+
755+ def test_windows_crlf_reports_line_ending_not_command_not_found (self ):
756+ """'\\ r: command not found' should say 'line ending', NOT 'command not found locally'."""
757+ msg = classify_error (
758+ "\r : command not found" , exit_code = 127 ,
759+ command = "script.sh" , protocol = "sh" ,
760+ )
761+ assert "line ending" in msg .lower ()
762+ assert "command not found locally" not in msg .lower ()
763+
764+ def test_windows_bad_interpreter_reports_line_ending_not_permission (self ):
765+ """'bad interpreter' should say 'line ending', NOT 'permission denied'."""
766+ msg = classify_error (
767+ "bad interpreter: No such file or directory" , exit_code = 126 ,
768+ command = "script.sh" , protocol = "sh" ,
769+ )
770+ assert "line ending" in msg .lower ()
771+ assert "permission denied" not in msg .lower ()
772+
773+ # --- D. chmod no-op on Windows ---
774+
775+ @patch ("fz.runners.platform.system" , return_value = "Windows" )
776+ def test_windows_exit_126_reports_chmod_noop (self , mock_platform ):
777+ """Exit code 126 on Windows should mention 'chmod +x is a no-op'."""
778+ msg = _classify_sh_error ("" , 126 , "./script.sh" )
779+ assert msg is not None
780+ assert "chmod" in msg .lower () or "no-op" in msg .lower ()
781+ assert "bash" in msg .lower ()
782+
783+ @patch ("fz.runners.platform.system" , return_value = "Windows" )
784+ def test_windows_exit_126_does_not_say_remote (self , mock_platform ):
785+ """Exit 126 on Windows should NOT say 'remote'."""
786+ msg = _classify_sh_error ("" , 126 , "./script.sh" )
787+ assert msg is not None
788+ assert "remote" not in msg .lower ()
789+
790+ # --- E. WinError codes ---
791+
792+ def test_windows_winerror2_not_misclassified_as_syntax (self ):
793+ """WinError 2 should NOT be classified as syntax error."""
794+ msg = _classify_sh_error (
795+ "[WinError 2] The system cannot find the file specified" ,
796+ 1 , "script.sh" ,
797+ )
798+ assert msg is not None
799+ assert "syntax error" not in msg .lower ()
800+
801+ def test_windows_winerror193_not_misclassified_as_oom (self ):
802+ """WinError 193 should NOT be classified as OOM."""
803+ msg = classify_error (
804+ "[WinError 193] %1 is not a valid Win32 application" ,
805+ exit_code = 1 , command = "script.sh" , protocol = "sh" ,
806+ )
807+ assert "memory" not in msg .lower ()
808+ assert "oom" not in msg .lower ()
809+
810+ # --- F. End-to-end mock with run_local_calculation ---
811+
812+ @patch ("fz.runners.platform.system" , return_value = "Windows" )
813+ @patch ("fz.runners.run_command" )
814+ def test_run_local_calc_windows_no_bash_error_in_result (
815+ self , mock_run_cmd , mock_platform , input_dir , simple_model
816+ ):
817+ """Mock Windows + no bash → result['error'] should be descriptive."""
818+ mock_run_cmd .side_effect = FileNotFoundError (
819+ "[WinError 2] The system cannot find the file specified"
820+ )
821+ result = run_local_calculation (
822+ working_dir = input_dir ,
823+ command = "script.sh" ,
824+ model = simple_model ,
825+ timeout = 10 ,
826+ original_cwd = str (input_dir ),
827+ input_files_list = ["input.txt" ],
828+ )
829+ assert "error" in result
830+ assert len (result ["error" ]) > 10
831+
832+ @patch ("fz.runners.platform.system" , return_value = "Windows" )
833+ @patch ("fz.runners.run_command" )
834+ def test_run_local_calc_windows_no_bash_not_done (
835+ self , mock_run_cmd , mock_platform , input_dir , simple_model
836+ ):
837+ """Mock Windows + no bash → result['status'] should NOT be 'done'."""
838+ mock_run_cmd .side_effect = FileNotFoundError (
839+ "[WinError 2] The system cannot find the file specified"
840+ )
841+ result = run_local_calculation (
842+ working_dir = input_dir ,
843+ command = "script.sh" ,
844+ model = simple_model ,
845+ timeout = 10 ,
846+ original_cwd = str (input_dir ),
847+ input_files_list = ["input.txt" ],
848+ )
849+ assert result ["status" ] != "done"
850+
851+
686852# ===========================================================================
687853# 2. sh:// (local shell) error reporting
688854# ===========================================================================
0 commit comments