Skip to content

Commit 4c06c6b

Browse files
committed
Fail closed on PurelyMail API errors
1 parent c7feaa2 commit 4c06c6b

5 files changed

Lines changed: 98 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
1616
- Updated the harbor adaptor to support the full V2 lenient & strict and reports numeric results.
1717

1818
### Fixed
19+
- Fail task setup when PurelyMail returns an API error instead of emitting credentials for an account that was not created.
1920
- Fixed an issue where malformed per-run metadata could prevent `batch-summary.json` from being written and, when configured, uploaded.
2021
- Fixed the issue that an invalid judge model would lose the `run-meta.json` file.
2122

src/clawbench/runner/run_support/email.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ def purelymail_request(endpoint: str, body: dict, api_key: str) -> dict:
1818
method="POST",
1919
)
2020
with urlopen(req, timeout=15) as resp:
21-
return json.loads(resp.read())
21+
result = json.loads(resp.read())
22+
if not isinstance(result, dict):
23+
raise RuntimeError(f"PurelyMail {endpoint} returned an invalid response")
24+
if result.get("type") == "error":
25+
code = f" ({result['code']})" if result.get("code") else ""
26+
message = f": {result['message']}" if result.get("message") else ""
27+
raise RuntimeError(f"PurelyMail {endpoint} failed{code}{message}")
28+
return result
2229

2330

2431
def create_email(api_key: str, domain: str) -> tuple[str, str]:

src/clawbench/runtime/harbor/cleanup-email.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ def purelymail_request(endpoint: str, body: dict, api_key: str) -> dict:
2828
method="POST",
2929
)
3030
with urlopen(req, timeout=15) as resp:
31-
return json.loads(resp.read())
31+
result = json.loads(resp.read())
32+
if not isinstance(result, dict):
33+
raise RuntimeError(f"PurelyMail {endpoint} returned an invalid response")
34+
if result.get("type") == "error":
35+
code = f" ({result['code']})" if result.get("code") else ""
36+
message = f": {result['message']}" if result.get("message") else ""
37+
raise RuntimeError(f"PurelyMail {endpoint} failed{code}{message}")
38+
return result
3239

3340

3441
def main() -> int:

src/clawbench/runtime/harbor/prepare-task.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,14 @@ def purelymail_request(endpoint: str, body: dict, api_key: str) -> dict:
3030
method="POST",
3131
)
3232
with urlopen(req, timeout=15) as resp:
33-
return json.loads(resp.read())
33+
result = json.loads(resp.read())
34+
if not isinstance(result, dict):
35+
raise RuntimeError(f"PurelyMail {endpoint} returned an invalid response")
36+
if result.get("type") == "error":
37+
code = f" ({result['code']})" if result.get("code") else ""
38+
message = f": {result['message']}" if result.get("message") else ""
39+
raise RuntimeError(f"PurelyMail {endpoint} failed{code}{message}")
40+
return result
3441

3542

3643
def create_email(api_key: str, domain: str) -> tuple[str, str]:

tests/test_purelymail_errors.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from __future__ import annotations
2+
3+
import importlib.util
4+
import json
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
from clawbench.runner.run_support import email as native_email
10+
11+
REPO_ROOT = Path(__file__).resolve().parents[1]
12+
13+
14+
class FakeResponse:
15+
def __init__(self, payload: object) -> None:
16+
self.payload = payload
17+
18+
def __enter__(self):
19+
return self
20+
21+
def __exit__(self, *_args) -> None:
22+
return None
23+
24+
def read(self) -> bytes:
25+
return json.dumps(self.payload).encode()
26+
27+
28+
def load_harbor_script(name: str):
29+
path = REPO_ROOT / "src" / "clawbench" / "runtime" / "harbor" / name
30+
spec = importlib.util.spec_from_file_location(name.replace("-", "_"), path)
31+
assert spec and spec.loader
32+
module = importlib.util.module_from_spec(spec)
33+
spec.loader.exec_module(module)
34+
return module
35+
36+
37+
@pytest.mark.parametrize(
38+
"module",
39+
[
40+
native_email,
41+
pytest.param(load_harbor_script("prepare-task.py"), id="harbor-prepare"),
42+
pytest.param(load_harbor_script("cleanup-email.py"), id="harbor-cleanup"),
43+
],
44+
)
45+
def test_purelymail_api_errors_fail_closed(monkeypatch, module) -> None:
46+
monkeypatch.setattr(
47+
module,
48+
"urlopen",
49+
lambda *_args, **_kwargs: FakeResponse(
50+
{
51+
"type": "error",
52+
"code": "invalidToken",
53+
"message": "Token not valid.",
54+
}
55+
),
56+
)
57+
58+
with pytest.raises(
59+
RuntimeError,
60+
match=r"PurelyMail createUser failed \(invalidToken\): Token not valid\.",
61+
):
62+
module.purelymail_request("createUser", {}, "secret-token")
63+
64+
65+
def test_purelymail_rejects_non_object_responses(monkeypatch) -> None:
66+
monkeypatch.setattr(
67+
native_email,
68+
"urlopen",
69+
lambda *_args, **_kwargs: FakeResponse(["unexpected"]),
70+
)
71+
72+
with pytest.raises(RuntimeError, match="returned an invalid response"):
73+
native_email.purelymail_request("createUser", {}, "secret-token")

0 commit comments

Comments
 (0)