Skip to content

Commit 0da0ad0

Browse files
Harden CUA manager param serialization
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 31f4d4a commit 0da0ad0

3 files changed

Lines changed: 227 additions & 2 deletions

File tree

hyperbrowser/client/managers/async_manager/agents/cua.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
wait_for_job_result_async,
77
)
88
from ...response_utils import parse_response_model
9+
from .....exceptions import HyperbrowserError
910

1011
from .....models import (
1112
POLLING_ATTEMPTS,
@@ -22,9 +23,20 @@ def __init__(self, client):
2223
self._client = client
2324

2425
async def start(self, params: StartCuaTaskParams) -> StartCuaTaskResponse:
26+
try:
27+
payload = params.model_dump(exclude_none=True, by_alias=True)
28+
except HyperbrowserError:
29+
raise
30+
except Exception as exc:
31+
raise HyperbrowserError(
32+
"Failed to serialize CUA start params",
33+
original_error=exc,
34+
) from exc
35+
if type(payload) is not dict:
36+
raise HyperbrowserError("Failed to serialize CUA start params")
2537
response = await self._client.transport.post(
2638
self._client._build_url("/task/cua"),
27-
data=params.model_dump(exclude_none=True, by_alias=True),
39+
data=payload,
2840
)
2941
return parse_response_model(
3042
response.data,

hyperbrowser/client/managers/sync_manager/agents/cua.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ....polling import build_operation_name, ensure_started_job_id, wait_for_job_result
44
from ...response_utils import parse_response_model
5+
from .....exceptions import HyperbrowserError
56

67
from .....models import (
78
POLLING_ATTEMPTS,
@@ -18,9 +19,20 @@ def __init__(self, client):
1819
self._client = client
1920

2021
def start(self, params: StartCuaTaskParams) -> StartCuaTaskResponse:
22+
try:
23+
payload = params.model_dump(exclude_none=True, by_alias=True)
24+
except HyperbrowserError:
25+
raise
26+
except Exception as exc:
27+
raise HyperbrowserError(
28+
"Failed to serialize CUA start params",
29+
original_error=exc,
30+
) from exc
31+
if type(payload) is not dict:
32+
raise HyperbrowserError("Failed to serialize CUA start params")
2133
response = self._client.transport.post(
2234
self._client._build_url("/task/cua"),
23-
data=params.model_dump(exclude_none=True, by_alias=True),
35+
data=payload,
2436
)
2537
return parse_response_model(
2638
response.data,

tests/test_cua_manager.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import asyncio
2+
from types import MappingProxyType, SimpleNamespace
3+
4+
import pytest
5+
6+
from hyperbrowser.client.managers.async_manager.agents.cua import (
7+
CuaManager as AsyncCuaManager,
8+
)
9+
from hyperbrowser.client.managers.sync_manager.agents.cua import (
10+
CuaManager as SyncCuaManager,
11+
)
12+
from hyperbrowser.exceptions import HyperbrowserError
13+
from hyperbrowser.models.agents.cua import StartCuaTaskParams
14+
15+
16+
class _SyncTransport:
17+
def __init__(self) -> None:
18+
self.calls = []
19+
20+
def post(self, url: str, data=None) -> SimpleNamespace:
21+
self.calls.append((url, data))
22+
return SimpleNamespace(data={"jobId": "job_sync_1"})
23+
24+
25+
class _AsyncTransport:
26+
def __init__(self) -> None:
27+
self.calls = []
28+
29+
async def post(self, url: str, data=None) -> SimpleNamespace:
30+
self.calls.append((url, data))
31+
return SimpleNamespace(data={"jobId": "job_async_1"})
32+
33+
34+
class _SyncClient:
35+
def __init__(self) -> None:
36+
self.transport = _SyncTransport()
37+
38+
def _build_url(self, path: str) -> str:
39+
return path
40+
41+
42+
class _AsyncClient:
43+
def __init__(self) -> None:
44+
self.transport = _AsyncTransport()
45+
46+
def _build_url(self, path: str) -> str:
47+
return path
48+
49+
50+
def test_sync_cua_start_serializes_params():
51+
client = _SyncClient()
52+
manager = SyncCuaManager(client)
53+
54+
response = manager.start(StartCuaTaskParams(task="open docs"))
55+
56+
assert response.job_id == "job_sync_1"
57+
_, payload = client.transport.calls[0]
58+
assert payload == {"task": "open docs"}
59+
60+
61+
def test_async_cua_start_serializes_params():
62+
client = _AsyncClient()
63+
manager = AsyncCuaManager(client)
64+
65+
async def run() -> None:
66+
response = await manager.start(StartCuaTaskParams(task="open docs"))
67+
assert response.job_id == "job_async_1"
68+
_, payload = client.transport.calls[0]
69+
assert payload == {"task": "open docs"}
70+
71+
asyncio.run(run())
72+
73+
74+
def test_sync_cua_start_wraps_param_serialization_errors(
75+
monkeypatch: pytest.MonkeyPatch,
76+
):
77+
manager = SyncCuaManager(_SyncClient())
78+
params = StartCuaTaskParams(task="open docs")
79+
80+
def _raise_model_dump_error(*args, **kwargs):
81+
_ = args
82+
_ = kwargs
83+
raise RuntimeError("broken model_dump")
84+
85+
monkeypatch.setattr(StartCuaTaskParams, "model_dump", _raise_model_dump_error)
86+
87+
with pytest.raises(
88+
HyperbrowserError, match="Failed to serialize CUA start params"
89+
) as exc_info:
90+
manager.start(params)
91+
92+
assert isinstance(exc_info.value.original_error, RuntimeError)
93+
94+
95+
def test_sync_cua_start_preserves_hyperbrowser_serialization_errors(
96+
monkeypatch: pytest.MonkeyPatch,
97+
):
98+
manager = SyncCuaManager(_SyncClient())
99+
params = StartCuaTaskParams(task="open docs")
100+
101+
def _raise_model_dump_error(*args, **kwargs):
102+
_ = args
103+
_ = kwargs
104+
raise HyperbrowserError("custom model_dump failure")
105+
106+
monkeypatch.setattr(StartCuaTaskParams, "model_dump", _raise_model_dump_error)
107+
108+
with pytest.raises(
109+
HyperbrowserError, match="custom model_dump failure"
110+
) as exc_info:
111+
manager.start(params)
112+
113+
assert exc_info.value.original_error is None
114+
115+
116+
def test_sync_cua_start_rejects_non_dict_serialized_params(
117+
monkeypatch: pytest.MonkeyPatch,
118+
):
119+
manager = SyncCuaManager(_SyncClient())
120+
params = StartCuaTaskParams(task="open docs")
121+
122+
monkeypatch.setattr(
123+
StartCuaTaskParams,
124+
"model_dump",
125+
lambda *args, **kwargs: MappingProxyType({"task": "open docs"}),
126+
)
127+
128+
with pytest.raises(
129+
HyperbrowserError, match="Failed to serialize CUA start params"
130+
) as exc_info:
131+
manager.start(params)
132+
133+
assert exc_info.value.original_error is None
134+
135+
136+
def test_async_cua_start_wraps_param_serialization_errors(
137+
monkeypatch: pytest.MonkeyPatch,
138+
):
139+
manager = AsyncCuaManager(_AsyncClient())
140+
params = StartCuaTaskParams(task="open docs")
141+
142+
def _raise_model_dump_error(*args, **kwargs):
143+
_ = args
144+
_ = kwargs
145+
raise RuntimeError("broken model_dump")
146+
147+
monkeypatch.setattr(StartCuaTaskParams, "model_dump", _raise_model_dump_error)
148+
149+
async def run() -> None:
150+
with pytest.raises(
151+
HyperbrowserError, match="Failed to serialize CUA start params"
152+
) as exc_info:
153+
await manager.start(params)
154+
assert isinstance(exc_info.value.original_error, RuntimeError)
155+
156+
asyncio.run(run())
157+
158+
159+
def test_async_cua_start_preserves_hyperbrowser_serialization_errors(
160+
monkeypatch: pytest.MonkeyPatch,
161+
):
162+
manager = AsyncCuaManager(_AsyncClient())
163+
params = StartCuaTaskParams(task="open docs")
164+
165+
def _raise_model_dump_error(*args, **kwargs):
166+
_ = args
167+
_ = kwargs
168+
raise HyperbrowserError("custom model_dump failure")
169+
170+
monkeypatch.setattr(StartCuaTaskParams, "model_dump", _raise_model_dump_error)
171+
172+
async def run() -> None:
173+
with pytest.raises(
174+
HyperbrowserError, match="custom model_dump failure"
175+
) as exc_info:
176+
await manager.start(params)
177+
assert exc_info.value.original_error is None
178+
179+
asyncio.run(run())
180+
181+
182+
def test_async_cua_start_rejects_non_dict_serialized_params(
183+
monkeypatch: pytest.MonkeyPatch,
184+
):
185+
manager = AsyncCuaManager(_AsyncClient())
186+
params = StartCuaTaskParams(task="open docs")
187+
188+
monkeypatch.setattr(
189+
StartCuaTaskParams,
190+
"model_dump",
191+
lambda *args, **kwargs: MappingProxyType({"task": "open docs"}),
192+
)
193+
194+
async def run() -> None:
195+
with pytest.raises(
196+
HyperbrowserError, match="Failed to serialize CUA start params"
197+
) as exc_info:
198+
await manager.start(params)
199+
assert exc_info.value.original_error is None
200+
201+
asyncio.run(run())

0 commit comments

Comments
 (0)