@@ -55,6 +55,7 @@ async def _run_process(
5555 quarantine_result : quarantine .QuarantineResult | None = None ,
5656 quarantine_side_effect : Exception | None = None ,
5757 upload_side_effect : Exception | None = None ,
58+ test_exit_code : int | None = None ,
5859 capsys : pytest .CaptureFixture [str ],
5960) -> ProcessResult :
6061 quarantine_mock = mock .AsyncMock ()
@@ -93,6 +94,7 @@ async def _run_process(
9394 test_language = None ,
9495 tests_target_branch = "main" ,
9596 files = files ,
97+ test_exit_code = test_exit_code ,
9698 )
9799
98100 captured = capsys .readouterr ()
@@ -792,3 +794,154 @@ async def test_no_test_cases_in_junit(
792794 " Exit code: 1\n "
793795 "══════════════════════════════════════════\n "
794796 )
797+
798+
799+ # ── Silent failure detection (--test-exit-code) ──
800+
801+
802+ async def test_test_exit_code_not_provided_all_pass (
803+ capsys : pytest .CaptureFixture [str ],
804+ ) -> None :
805+ """Backwards compat: no --test-exit-code flag, all tests pass -> exit 0."""
806+ result = await _run_process (
807+ files = (str (REPORT_ALL_PASS_XML ),),
808+ quarantine_result = quarantine .QuarantineResult (
809+ failing_spans = [],
810+ quarantined_spans = [],
811+ non_quarantined_spans = [],
812+ failing_tests_not_quarantined_count = 0 ,
813+ ),
814+ test_exit_code = None ,
815+ capsys = capsys ,
816+ )
817+
818+ assert result .exit_code == 0
819+ assert "all tests passed" in result .stdout
820+
821+
822+ async def test_test_exit_code_zero_all_pass (
823+ capsys : pytest .CaptureFixture [str ],
824+ ) -> None :
825+ """--test-exit-code 0, all tests pass -> exit 0."""
826+ result = await _run_process (
827+ files = (str (REPORT_ALL_PASS_XML ),),
828+ quarantine_result = quarantine .QuarantineResult (
829+ failing_spans = [],
830+ quarantined_spans = [],
831+ non_quarantined_spans = [],
832+ failing_tests_not_quarantined_count = 0 ,
833+ ),
834+ test_exit_code = 0 ,
835+ capsys = capsys ,
836+ )
837+
838+ assert result .exit_code == 0
839+ assert "all tests passed" in result .stdout
840+
841+
842+ async def test_test_exit_code_nonzero_with_failures (
843+ capsys : pytest .CaptureFixture [str ],
844+ ) -> None :
845+ """--test-exit-code 1 with failures in JUnit -> normal quarantine flow."""
846+ result = await _run_process (
847+ quarantine_result = quarantine .QuarantineResult (
848+ failing_spans = [FAILING_SPAN ],
849+ quarantined_spans = [],
850+ non_quarantined_spans = [FAILING_SPAN ],
851+ failing_tests_not_quarantined_count = 1 ,
852+ ),
853+ test_exit_code = 1 ,
854+ capsys = capsys ,
855+ )
856+
857+ assert result .exit_code == 1
858+ assert "0/1 failures quarantined" in result .stdout
859+ assert "test runner exited with an error" not in result .stdout
860+
861+
862+ async def test_test_exit_code_nonzero_no_failures (
863+ capsys : pytest .CaptureFixture [str ],
864+ ) -> None :
865+ """--test-exit-code 1 with no failures -> silent failure detected, exit 1."""
866+ result = await _run_process (
867+ files = (str (REPORT_ALL_PASS_XML ),),
868+ quarantine_result = quarantine .QuarantineResult (
869+ failing_spans = [],
870+ quarantined_spans = [],
871+ non_quarantined_spans = [],
872+ failing_tests_not_quarantined_count = 0 ,
873+ ),
874+ test_exit_code = 1 ,
875+ capsys = capsys ,
876+ )
877+
878+ assert result .exit_code == 1
879+ assert result .stdout == (
880+ "══════════════════════════════════════════\n "
881+ " 🚀 CI Insights\n "
882+ "\n "
883+ " Uploads JUnit test results to Mergify CI Insights and evaluates\n "
884+ " quarantine status for failing tests. This step determines the\n "
885+ " final CI status — quarantined failures are ignored.\n "
886+ " Learn more: https://docs.mergify.com/ci-insights/quarantine\n "
887+ "══════════════════════════════════════════\n "
888+ "\n "
889+ " Run ID: 00000002dfdc1c3e\n "
890+ " ☁️ 1 report uploaded\n "
891+ " 🧪 2 tests (0 failures)\n "
892+ "\n "
893+ "──────────────────────────────────────────\n "
894+ "\n "
895+ " ⚠️ Test runner exited with an error (exit code: 1)\n "
896+ " but no test failures appear in the JUnit report.\n "
897+ " The report may be incomplete — check your test runner logs.\n "
898+ "\n "
899+ "══════════════════════════════════════════\n "
900+ "❌ FAIL — test runner exited with an error but no failures were reported\n "
901+ " Exit code: 1\n "
902+ "══════════════════════════════════════════\n "
903+ )
904+
905+
906+ async def test_test_exit_code_oom_kill_no_failures (
907+ capsys : pytest .CaptureFixture [str ],
908+ ) -> None :
909+ """--test-exit-code 137 (OOM) with no failures -> shows exit code 137 in warning."""
910+ result = await _run_process (
911+ files = (str (REPORT_ALL_PASS_XML ),),
912+ quarantine_result = quarantine .QuarantineResult (
913+ failing_spans = [],
914+ quarantined_spans = [],
915+ non_quarantined_spans = [],
916+ failing_tests_not_quarantined_count = 0 ,
917+ ),
918+ test_exit_code = 137 ,
919+ capsys = capsys ,
920+ )
921+
922+ assert result .exit_code == 1
923+ assert "exit code: 137" in result .stdout
924+ assert (
925+ "test runner exited with an error but no failures were reported"
926+ in result .stdout
927+ )
928+
929+
930+ async def test_test_exit_code_nonzero_with_quarantined_failures (
931+ capsys : pytest .CaptureFixture [str ],
932+ ) -> None :
933+ """--test-exit-code 1 with all failures quarantined -> quarantine logic applies, exit 0."""
934+ result = await _run_process (
935+ quarantine_result = quarantine .QuarantineResult (
936+ failing_spans = [FAILING_SPAN ],
937+ quarantined_spans = [FAILING_SPAN ],
938+ non_quarantined_spans = [],
939+ failing_tests_not_quarantined_count = 0 ,
940+ ),
941+ test_exit_code = 1 ,
942+ capsys = capsys ,
943+ )
944+
945+ assert result .exit_code == 0
946+ assert "failures quarantined, CI status unaffected" in result .stdout
947+ assert "test runner exited with an error" not in result .stdout
0 commit comments