diff --git a/python/robomp/src/host_tools.py b/python/robomp/src/host_tools.py index a2239afac..ccc9c3b30 100644 --- a/python/robomp/src/host_tools.py +++ b/python/robomp/src/host_tools.py @@ -143,6 +143,10 @@ def _issue_needs_info(bindings: ToolBindings) -> bool: return row is not None and row.state == "needs_info" +def _optional_label_error(exc: Exception) -> str: + return f"{type(exc).__name__}: {exc}" + + def _remove_needs_info_label(bindings: ToolBindings) -> bool: try: _run_coro( @@ -154,6 +158,12 @@ def _remove_needs_info_label(bindings: ToolBindings) -> bool: return True log.warning("needs-info label cleanup failed", extra={"issue": bindings.issue_key, "err": str(exc)}) return False + except Exception as exc: # noqa: BLE001 - best-effort optional label cleanup + log.warning( + "needs-info label cleanup failed", + extra={"issue": bindings.issue_key, "err": _optional_label_error(exc)}, + ) + return False return True @@ -934,6 +944,10 @@ def _build_mark_unable(bindings: ToolBindings) -> HostTool[Any, Any]: # info-request comment, so label setup must not block resumption. log.warning("needs-info label failed", extra={"issue": bindings.issue_key, "err": str(exc)}) result["label_error"] = f"{exc.status} {exc.message}" + except Exception as exc: # noqa: BLE001 - best-effort optional label setup + error = _optional_label_error(exc) + log.warning("needs-info label failed", extra={"issue": bindings.issue_key, "err": error}) + result["label_error"] = error bindings.db.set_issue_state(bindings.issue_key, "needs_info") _audit(bindings, "mark_unable_to_reproduce", args, result=result) return f"posted needs-info comment id={comment.id}" diff --git a/python/robomp/tests/test_host_tools.py b/python/robomp/tests/test_host_tools.py index 708796d49..cbfb45363 100644 --- a/python/robomp/tests/test_host_tools.py +++ b/python/robomp/tests/test_host_tools.py @@ -437,6 +437,31 @@ def test_repro_record_advances_needs_info_when_label_is_missing(db: Database, tm assert issue and issue.state == "reproducing" +def test_repro_record_advances_needs_info_when_cleanup_transport_fails(db: Database, tmp_path: Path) -> None: + def handler(request: httpx.Request) -> httpx.Response: + raise httpx.ConnectError("connection dropped", request=request) + + bindings, loop, t = _bindings(db, tmp_path, httpx.MockTransport(handler)) + db.set_issue_state(bindings.issue_key, "needs_info") + try: + tool = next(x for x in build(bindings) if x.name == "repro_record") + result = tool.execute( + { + "title": "panic on empty input", + "command": "bun test foo.test.ts", + "output": "Error: boom", + "exit_code": 1, + }, + _ctx(), + ) + finally: + _stop_loop(loop, t) + + assert result == "recorded" + issue = db.get_issue(bindings.issue_key) + assert issue and issue.state == "reproducing" + + def test_repro_record_chowns_to_slot_when_root(db: Database, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: chowns: list[tuple[Path, int, int]] = [] monkeypatch.setattr(host_tools, "_slot_permissions_active", lambda slot_uid: slot_uid is not None) @@ -519,6 +544,34 @@ def test_mark_unable_keeps_needs_info_when_label_is_missing(db: Database, tmp_pa assert issue and issue.state == "needs_info" +def test_mark_unable_keeps_needs_info_when_label_transport_fails(db: Database, tmp_path: Path) -> None: + comments = 0 + + def handler(request: httpx.Request) -> httpx.Response: + nonlocal comments + if request.url.path.endswith("/labels"): + raise httpx.ConnectError("connection dropped", request=request) + comments += 1 + return httpx.Response(201, json={"id": 321, "user": {"login": "robomp-bot"}, "body": "x", "created_at": "t"}) + + bindings, loop, t = _bindings(db, tmp_path, httpx.MockTransport(handler)) + try: + tool = next(x for x in build(bindings) if x.name == "mark_unable_to_reproduce") + tool.execute({"diagnosis": "needed exact version", "info_needed": "post bun --version"}, _ctx()) + finally: + _stop_loop(loop, t) + + assert comments == 1 + issue = db.get_issue(bindings.issue_key) + assert issue and issue.state == "needs_info" + row = db._conn.execute( + "SELECT result_json FROM tool_calls WHERE tool='mark_unable_to_reproduce' ORDER BY id DESC LIMIT 1" + ).fetchone() + assert row is not None + result = json.loads(row["result_json"]) + assert "ConnectError" in result["label_error"] + + def test_abort_task_signals_controller_and_abandons_without_comment(db: Database, tmp_path: Path) -> None: # Any HTTP call is a regression: abort_task MUST NOT touch GitHub. def handler(request: httpx.Request) -> httpx.Response: