From 6109c6d11a279d8cae77e53b1539b2fb0411743a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 18:08:20 +0800 Subject: [PATCH] test: cover YarnError -> 502 across all YARN-touching tools In 3b698e1 I added the YarnError exception handler and tested it against get_job_status only. The other four YARN-touching tools (get_external_job_status, get_external_job_result, get_external_job_logs, list_applications) were assumed to be covered by the same handler but never actually exercised against a YarnError. Add one integration test per remaining tool. Each one: - saves a Connection - mocks the underlying yarn_client symbol the tool uses (get_application_status / get_application_logs / list_applications_yarn) to raise YarnError with a distinctive message - hits the tool's route via TestClient - asserts HTTP 502 + the YarnError message in the response detail The new tests prove the YarnError -> 502 contract holds uniformly: - test_external_job_status_returns_502_on_yarn_error (get_application_status raising "not found") - test_external_job_result_returns_502_on_yarn_error (get_application_status raising "Connection refused") - test_external_job_logs_returns_502_on_yarn_error (get_application_logs raising "HTTP 500 cluster overloaded") - test_list_applications_returns_502_on_yarn_error (list_applications_yarn raising "HTTP 503") Each one uses a different YARN exception message so the test distinguishes which code path produced the error. Combined with the existing get_job_status test, the YarnError -> 502 contract is now verified end-to-end for all five YARN REST-call sites. Tests: 405 passed (was 401, +4 net). Co-Authored-By: Claude Fable 5 --- tests/integration/test_mcp_routes.py | 110 +++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/integration/test_mcp_routes.py b/tests/integration/test_mcp_routes.py index 87d9f2d..0d6708e 100644 --- a/tests/integration/test_mcp_routes.py +++ b/tests/integration/test_mcp_routes.py @@ -579,3 +579,113 @@ def test_yarn_error_returns_502_with_detail(tmp_path, monkeypatch): detail = r.json()["detail"] assert "YARN connection failed" in detail assert "Connection refused" in detail + + +def test_external_job_status_returns_502_on_yarn_error(tmp_path): + """get_external_job_status uses get_application_status under the + hood — same handler, same 502, same detail.""" + from unittest.mock import patch + from spark_executor.core.yarn_client import YarnError + + connection_store.store.save( + Connection( + name="prod", + master="yarn", + yarn_rm_url="http://ccam1:8088", + ) + ) + c = TestClient(app) + with patch( + "spark_executor.tools.external_jobs.get_application_status", + side_effect=YarnError("YARN application 'application_xxx' not found"), + ): + r = c.post( + "/get_external_job_status", + json={"application_id": "application_1740000000001_0001", "connection_name": "prod"}, + ) + assert r.status_code == 502 + assert "not found" in r.json()["detail"] + + +def test_external_job_result_returns_502_on_yarn_error(tmp_path): + """get_external_job_result also uses get_application_status (same + underlying YARN endpoint), and exercises the same YarnError path.""" + from unittest.mock import patch + from spark_executor.core.yarn_client import YarnError + + connection_store.store.save( + Connection( + name="prod", + master="yarn", + yarn_rm_url="http://ccam1:8088", + ) + ) + c = TestClient(app) + with patch( + "spark_executor.tools.external_jobs.get_application_status", + side_effect=YarnError("YARN connection failed: ConnectError: Connection refused"), + ): + r = c.post( + "/get_external_job_result", + json={"application_id": "application_1740000000001_0001", "connection_name": "prod"}, + ) + assert r.status_code == 502 + assert "YARN connection failed" in r.json()["detail"] + + +def test_external_job_logs_returns_502_on_yarn_error(tmp_path): + """get_external_job_logs uses get_application_logs — different + function, but raises YarnError the same way. Handler must catch + it the same way.""" + from unittest.mock import patch + from spark_executor.core.yarn_client import YarnError + + connection_store.store.save( + Connection( + name="prod", + master="yarn", + yarn_rm_url="http://ccam1:8088", + ) + ) + c = TestClient(app) + with patch( + "spark_executor.tools.external_jobs.get_application_logs", + side_effect=YarnError("YARN GET logs returned HTTP 500: cluster overloaded"), + ): + r = c.post( + "/get_external_job_logs", + json={ + "application_id": "application_1740000000001_0001", + "connection_name": "prod", + "tail_chars": 5000, + }, + ) + assert r.status_code == 502 + assert "YARN GET logs returned HTTP 500" in r.json()["detail"] + assert "cluster overloaded" in r.json()["detail"] + + +def test_list_applications_returns_502_on_yarn_error(tmp_path): + """list_applications uses list_applications_yarn — yet another + YARN-touching tool. Same YarnError -> 502 contract.""" + from unittest.mock import patch + from spark_executor.core.yarn_client import YarnError + + connection_store.store.save( + Connection( + name="prod", + master="yarn", + yarn_rm_url="http://ccam1:8088", + ) + ) + c = TestClient(app) + with patch( + "spark_executor.tools.external_jobs.list_applications_yarn", + side_effect=YarnError("YARN list applications failed: 503 Service Unavailable"), + ): + r = c.post( + "/list_applications", + json={"connection_name": "prod"}, + ) + assert r.status_code == 502 + assert "YARN list applications failed" in r.json()["detail"]