Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
416 changes: 416 additions & 0 deletions operator_use/computer/windows/virtual_display.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ async def test_agent_run_with_tool_call_then_text(tmp_path):

# Register a simple echo tool
from pydantic import BaseModel
from operator_use.tools.service import Tool
from operator_use.agent.tools.service import Tool

class EchoParams(BaseModel):
message: str
Expand Down
17 changes: 12 additions & 5 deletions tests/test_browser_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,27 @@ def test_unregister_tools_removes_browser_tool():


# ---------------------------------------------------------------------------
# register_hooks — BEFORE_LLM_CALL gated on _enabled
# register_hooks — no hooks registered on BEFORE_LLM_CALL
# (BEFORE_LLM_CALL registration was removed in 9f5d002: browser state is
# now captured inside the browser_task loop, not on every main-agent LLM call)
# ---------------------------------------------------------------------------


def test_disabled_plugin_registers_no_hooks():
plugin = BrowserPlugin(enabled=False)
hooks = Hooks()
plugin.register_hooks(hooks)
assert plugin._state_hook not in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
# register_hooks stores the hooks reference but does not register any handler
assert plugin._hooks is hooks


def test_enabled_plugin_registers_state_hook():
plugin = BrowserPlugin(enabled=False)
plugin._enabled = True
hooks = Hooks()
plugin.register_hooks(hooks)
assert plugin._state_hook in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
# No hook is registered on BEFORE_LLM_CALL; hook reference is stored only
assert plugin._hooks is hooks


def test_unregister_hooks_removes_state_hook():
Expand All @@ -86,7 +90,8 @@ def test_unregister_hooks_removes_state_hook():
hooks = Hooks()
plugin.register_hooks(hooks)
plugin.unregister_hooks(hooks)
assert plugin._state_hook not in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
# unregister_hooks is a no-op; _hooks reference is preserved
assert plugin._hooks is hooks


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -135,7 +140,8 @@ async def test_enable_registers_hooks_and_injects_prompt():
await plugin.enable()

assert plugin._enabled is True
assert plugin._state_hook in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
# _state_hook is not registered on BEFORE_LLM_CALL (see 9f5d002)
assert plugin._state_hook not in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
context.register_plugin_prompt.assert_called_once_with(SYSTEM_PROMPT)


Expand All @@ -151,6 +157,7 @@ async def test_disable_unregisters_hooks_and_removes_prompt():
await plugin.disable()

assert plugin._enabled is False
# _state_hook was never in BEFORE_LLM_CALL (see 9f5d002)
assert plugin._state_hook not in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
context.unregister_plugin_prompt.assert_called_once_with(SYSTEM_PROMPT)

Expand Down
8 changes: 3 additions & 5 deletions tests/test_computer_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@ def test_enabled_plugin_returns_system_prompt():


# ---------------------------------------------------------------------------
# register_hooks — BEFORE_LLM_CALL + AFTER_TOOL_CALL, gated on _enabled
# register_hooks — AFTER_TOOL_CALL only, gated on _enabled
# (BEFORE_LLM_CALL was removed in 9f5d002: state captured inside
# computer_task loop, not on every main-agent LLM call)
# ---------------------------------------------------------------------------


def test_disabled_plugin_registers_no_hooks():
plugin = ComputerPlugin(enabled=False)
hooks = Hooks()
plugin.register_hooks(hooks)
assert plugin._state_hook not in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
assert plugin._wait_for_ui_hook not in hooks._handlers[HookEvent.AFTER_TOOL_CALL]


Expand All @@ -51,7 +52,6 @@ def test_enabled_plugin_registers_both_hooks():
plugin._enabled = True
hooks = Hooks()
plugin.register_hooks(hooks)
assert plugin._state_hook in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
assert plugin._wait_for_ui_hook in hooks._handlers[HookEvent.AFTER_TOOL_CALL]


Expand All @@ -61,7 +61,6 @@ def test_unregister_hooks_removes_both():
hooks = Hooks()
plugin.register_hooks(hooks)
plugin.unregister_hooks(hooks)
assert plugin._state_hook not in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
assert plugin._wait_for_ui_hook not in hooks._handlers[HookEvent.AFTER_TOOL_CALL]


Expand Down Expand Up @@ -111,7 +110,6 @@ async def test_enable_registers_both_hooks_and_prompt():
await plugin.enable()

assert plugin._enabled is True
assert plugin._state_hook in hooks._handlers[HookEvent.BEFORE_LLM_CALL]
assert plugin._wait_for_ui_hook in hooks._handlers[HookEvent.AFTER_TOOL_CALL]
context.register_plugin_prompt.assert_called_once_with(SYSTEM_PROMPT)

Expand Down
39 changes: 20 additions & 19 deletions tests/test_control_center.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from operator_use.agent.tools.builtin.control_center import (
# Import via operator_use.agent.tools so the circular-import chain is resolved
# in the correct order before we access the module directly.
import operator_use.agent.tools # noqa: F401 — side-effect import resolves cycle
from operator_use.tools.control_center import (
control_center,
_set_plugin_enabled,
_get_plugin_enabled,
)

_MODULE = "operator_use.tools.control_center"


# ---------------------------------------------------------------------------
# Helpers
Expand Down Expand Up @@ -73,7 +78,7 @@ async def test_enable_browser_use_calls_agent(tmp_path):
mock_agent = MagicMock()
mock_agent.enable_browser_use = AsyncMock()

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", cfg_file):
with patch(f"{_MODULE}.CONFIG_PATH", cfg_file):
result = await _call_cc(browser_use=True, _agent=mock_agent)

mock_agent.enable_browser_use.assert_awaited_once()
Expand All @@ -90,7 +95,7 @@ async def test_enable_both_computer_use_and_browser_use_independently(tmp_path):
mock_agent.enable_computer_use = AsyncMock()
mock_agent.enable_browser_use = AsyncMock()

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", cfg_file):
with patch(f"{_MODULE}.CONFIG_PATH", cfg_file):
result = await _call_cc(computer_use=True, browser_use=True, _agent=mock_agent)

saved = json.loads(cfg_file.read_text())
Expand All @@ -111,7 +116,7 @@ async def test_disable_browser_use_calls_agent(tmp_path):
mock_agent = MagicMock()
mock_agent.disable_browser_use = AsyncMock()

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", cfg_file):
with patch(f"{_MODULE}.CONFIG_PATH", cfg_file):
result = await _call_cc(browser_use=False, _agent=mock_agent)

mock_agent.disable_browser_use.assert_awaited_once()
Expand All @@ -124,7 +129,7 @@ async def test_status_only_returns_current_state(tmp_path):
cfg_file = tmp_path / "config.json"
cfg_file.write_text(json.dumps(cfg))

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", cfg_file):
with patch(f"{_MODULE}.CONFIG_PATH", cfg_file):
result = await _call_cc()

assert result.success
Expand All @@ -148,10 +153,8 @@ async def test_audit_log_emitted_on_plugin_change(tmp_path, caplog):
mock_agent = MagicMock()
mock_agent.enable_browser_use = AsyncMock()

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", cfg_file):
with caplog.at_level(
logging.WARNING, logger="operator_use.agent.tools.builtin.control_center"
):
with patch(f"{_MODULE}.CONFIG_PATH", cfg_file):
with caplog.at_level(logging.WARNING, logger=_MODULE):
await _call_cc(
browser_use=True,
_agent=mock_agent,
Expand All @@ -172,10 +175,8 @@ async def test_audit_log_emitted_on_status_check(tmp_path, caplog):
cfg_file = tmp_path / "config.json"
cfg_file.write_text(json.dumps(cfg))

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", cfg_file):
with caplog.at_level(
logging.WARNING, logger="operator_use.agent.tools.builtin.control_center"
):
with patch(f"{_MODULE}.CONFIG_PATH", cfg_file):
with caplog.at_level(logging.WARNING, logger=_MODULE):
await _call_cc(_channel="discord", _chat_id="999", _agent_id="op")

assert any("control_center" in r.message for r in caplog.records)
Expand All @@ -197,8 +198,8 @@ async def test_restart_calls_graceful_fn_not_os_exit(tmp_path):
async def mock_graceful():
graceful_called.append(True)

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", cfg_file):
with patch("operator_use.agent.tools.builtin.control_center._do_restart") as mock_restart:
with patch(f"{_MODULE}.CONFIG_PATH", cfg_file):
with patch(f"{_MODULE}._do_restart") as mock_restart:
mock_restart.return_value = None
result = await _call_cc(restart=True, _graceful_restart_fn=mock_graceful)

Expand All @@ -217,8 +218,8 @@ async def test_restart_without_graceful_fn_still_works(tmp_path):
cfg_file = tmp_path / "config.json"
cfg_file.write_text(json.dumps(cfg))

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", cfg_file):
with patch("operator_use.agent.tools.builtin.control_center._do_restart"):
with patch(f"{_MODULE}.CONFIG_PATH", cfg_file):
with patch(f"{_MODULE}._do_restart"):
result = await _call_cc(restart=True)

assert result.success
Expand All @@ -234,7 +235,7 @@ async def test_returns_error_when_no_agents(tmp_path):
cfg_file = tmp_path / "config.json"
cfg_file.write_text(json.dumps({"agents": {"list": []}}))

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", cfg_file):
with patch(f"{_MODULE}.CONFIG_PATH", cfg_file):
result = await _call_cc(browser_use=True)

assert not result.success
Expand All @@ -244,7 +245,7 @@ async def test_returns_error_when_no_agents(tmp_path):
async def test_returns_error_when_config_missing(tmp_path):
missing = tmp_path / "no_config.json"

with patch("operator_use.agent.tools.builtin.control_center.CONFIG_PATH", missing):
with patch(f"{_MODULE}.CONFIG_PATH", missing):
result = await _call_cc(browser_use=True)

assert not result.success
2 changes: 1 addition & 1 deletion tests/test_local_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from operator_use.agent.tools.builtin.local_agents import LOCAL_AGENT_DELEGATION_CHAIN, localagents
from operator_use.tools.local_agents import LOCAL_AGENT_DELEGATION_CHAIN, localagents
from operator_use.messages.service import AIMessage


Expand Down
2 changes: 1 addition & 1 deletion tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from operator_use.agent.tools.registry import ToolRegistry
from operator_use.agent.hooks.service import Hooks
from operator_use.agent.hooks.events import HookEvent
from operator_use.tools.service import Tool
from operator_use.agent.tools.service import Tool
from pydantic import BaseModel


Expand Down
2 changes: 1 addition & 1 deletion tests/test_tool_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pydantic import BaseModel

from operator_use.agent.tools.registry import ToolRegistry
from operator_use.tools.service import Tool
from operator_use.agent.tools.service import Tool


# --- Helpers ---
Expand Down
2 changes: 1 addition & 1 deletion tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pydantic import BaseModel
from typing import Literal

from operator_use.tools.service import Tool, ToolResult
from operator_use.agent.tools.service import Tool, ToolResult


# --- ToolResult ---
Expand Down
Loading
Loading