diff --git a/scripts/run_leo_direct_claim_handler_suite.py b/scripts/run_leo_direct_claim_handler_suite.py index a786ca9..c88ae5e 100755 --- a/scripts/run_leo_direct_claim_handler_suite.py +++ b/scripts/run_leo_direct_claim_handler_suite.py @@ -882,30 +882,33 @@ def fetch_remote_report( ) -> dict[str, Any] | None: report_path = f"/tmp/{report_prefix}-{run_id}.json" unlink_line = " p.unlink()\n" if unlink else "" - proc = subprocess.run( - [ - "ssh", - "-i", - str(SSH_KEY), - "-o", - "BatchMode=yes", - "-o", - "StrictHostKeyChecking=accept-new", - VPS, - "sudo -u teleo -H /home/teleo/.hermes/hermes-agent/venv/bin/python -", - ], - input=( - "import json\n" - "from pathlib import Path\n" - f"p = Path({report_path!r})\n" - "if p.exists():\n" - " print(p.read_text(encoding='utf-8'))\n" - f"{unlink_line}" - ), - text=True, - capture_output=True, - timeout=60, - ) + try: + proc = subprocess.run( + [ + "ssh", + "-i", + str(SSH_KEY), + "-o", + "BatchMode=yes", + "-o", + "StrictHostKeyChecking=accept-new", + VPS, + "sudo -u teleo -H /home/teleo/.hermes/hermes-agent/venv/bin/python -", + ], + input=( + "import json\n" + "from pathlib import Path\n" + f"p = Path({report_path!r})\n" + "if p.exists():\n" + " print(p.read_text(encoding='utf-8'))\n" + f"{unlink_line}" + ), + text=True, + capture_output=True, + timeout=180, + ) + except subprocess.TimeoutExpired: + return None if proc.returncode != 0 or not proc.stdout.strip(): return None return json.loads(redact(proc.stdout.strip())) @@ -966,7 +969,9 @@ def run_remote( result["parsed"] = fetched result["parsed_from_report_file"] = True else: - fetch_remote_report(run_id, report_prefix=report_prefix) + result["remote_report_cleanup_confirmed"] = ( + fetch_remote_report(run_id, report_prefix=report_prefix) is not None + ) return result diff --git a/tests/test_run_leo_direct_claim_handler_suite.py b/tests/test_run_leo_direct_claim_handler_suite.py index 7b5341c..f9eeb76 100644 --- a/tests/test_run_leo_direct_claim_handler_suite.py +++ b/tests/test_run_leo_direct_claim_handler_suite.py @@ -89,6 +89,25 @@ def test_successful_stdout_is_redacted_before_json_parsing(monkeypatch) -> None: assert secret not in json.dumps(result) +def test_successful_stdout_survives_remote_report_cleanup_timeout(monkeypatch) -> None: + completed = subprocess.CompletedProcess([], 0, stdout=json.dumps({"run_id": "complete"}) + "\n", stderr="") + calls = 0 + + def fake_run(*args, **kwargs): + nonlocal calls + calls += 1 + if calls == 1: + return completed + raise subprocess.TimeoutExpired(args[0], timeout=180) + + monkeypatch.setattr(suite.subprocess, "run", fake_run) + + result = suite.run_remote(prompts=[]) + + assert result["parsed"] == {"run_id": "complete"} + assert result["remote_report_cleanup_confirmed"] is False + + def test_safety_gate_passes_only_for_complete_no_send_no_write_run() -> None: report = safe_report()