From c52cd48e25c86e3e5cf3676dfa221a9a32f0bff8 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sat, 9 May 2026 15:18:02 -0300 Subject: [PATCH] fix(computer-use): add set_value to ComputerUseBackend ABC and _NoopBackend stub _dispatch() routes action="set_value" to backend.set_value(), but: - ComputerUseBackend did not declare set_value as @abstractmethod, so subclasses could silently omit it without a TypeError at class load time. - _NoopBackend (the test/CI stub) had no set_value method at all, causing AttributeError in any test that exercises the set_value action path. Fix: - Add set_value as @abstractmethod to ComputerUseBackend in backend.py. - Add a recording stub in _NoopBackend in tool.py. - Add two TestDispatch cases: one verifying the call reaches the backend, one verifying the missing-value guard returns a clean error. --- tests/tools/test_computer_use.py | 15 +++++++++++++++ tools/computer_use/backend.py | 8 ++++++++ tools/computer_use/tool.py | 4 ++++ 3 files changed, 27 insertions(+) diff --git a/tests/tools/test_computer_use.py b/tests/tools/test_computer_use.py index 4a140c36be3..49d5bbdfbf9 100644 --- a/tests/tools/test_computer_use.py +++ b/tests/tools/test_computer_use.py @@ -226,6 +226,21 @@ class TestDispatch: parsed = json.loads(out) assert "error" in parsed + def test_set_value_routes_to_backend(self, noop_backend): + """set_value must reach the backend — regression for missing _NoopBackend stub.""" + from tools.computer_use.tool import handle_computer_use + out = handle_computer_use({"action": "set_value", "value": "Option A", "element": 5}) + parsed = json.loads(out) + assert parsed.get("ok") is True + assert parsed.get("action") == "set_value" + assert any(c[0] == "set_value" for c in noop_backend.calls) + + def test_set_value_missing_value_returns_error(self, noop_backend): + from tools.computer_use.tool import handle_computer_use + out = handle_computer_use({"action": "set_value"}) + parsed = json.loads(out) + assert "error" in parsed + # --------------------------------------------------------------------------- # Safety guards (type / key block lists) diff --git a/tools/computer_use/backend.py b/tools/computer_use/backend.py index 9952510e9cc..c9686e41b04 100644 --- a/tools/computer_use/backend.py +++ b/tools/computer_use/backend.py @@ -142,6 +142,14 @@ class ComputerUseBackend(ABC): def focus_app(self, app: str, raise_window: bool = False) -> ActionResult: """Route input to `app` (by name or bundle ID). Default: focus without raise.""" + # ── Native-value mutation ──────────────────────────────────────── + @abstractmethod + def set_value(self, value: str, element: Optional[int] = None) -> ActionResult: + """Set a native value on an element (e.g. AXPopUpButton selection). + + `element` is the 1-based SOM index returned by a prior capture call. + """ + # ── Timing ────────────────────────────────────────────────────── def wait(self, seconds: float) -> ActionResult: """Default implementation: time.sleep.""" diff --git a/tools/computer_use/tool.py b/tools/computer_use/tool.py index 82c6792eeb9..41308733df1 100644 --- a/tools/computer_use/tool.py +++ b/tools/computer_use/tool.py @@ -200,6 +200,10 @@ class _NoopBackend(ComputerUseBackend): # pragma: no cover self.calls.append(("focus_app", {"app": app, "raise": raise_window})) return ActionResult(ok=True, action="focus_app") + def set_value(self, value: str, element: Optional[int] = None) -> ActionResult: + self.calls.append(("set_value", {"value": value, "element": element})) + return ActionResult(ok=True, action="set_value") + # --------------------------------------------------------------------------- # Dispatch