""" Tests for nexus.computer_use — Desktop Automation Primitives (#1125) All tests run fully headless: pyautogui is mocked throughout. No display is required. """ from __future__ import annotations import json import sys from pathlib import Path from unittest.mock import MagicMock, patch, call import pytest sys.path.insert(0, str(Path(__file__).parent.parent)) from nexus.computer_use import ( _DANGEROUS_BUTTONS, _SENSITIVE_KEYWORDS, computer_click, computer_screenshot, computer_scroll, computer_type, read_action_log, ) # --------------------------------------------------------------------------- # Helpers / fixtures # --------------------------------------------------------------------------- @pytest.fixture def tmp_log(tmp_path): """Return a temporary JSONL audit log path.""" return tmp_path / "actions.jsonl" def _last_log_entry(log_path: Path) -> dict: lines = [l.strip() for l in log_path.read_text().splitlines() if l.strip()] return json.loads(lines[-1]) def _make_mock_pag(screenshot_raises=None): """Build a minimal pyautogui mock.""" mock = MagicMock() mock.FAILSAFE = True mock.PAUSE = 0.05 if screenshot_raises: mock.screenshot.side_effect = screenshot_raises else: img_mock = MagicMock() img_mock.save = MagicMock() mock.screenshot.return_value = img_mock return mock # --------------------------------------------------------------------------- # computer_screenshot # --------------------------------------------------------------------------- class TestComputerScreenshot: def test_returns_b64_when_no_save_path(self, tmp_log): mock_pag = _make_mock_pag() # Make save() write fake PNG bytes import io buf = io.BytesIO(b"\x89PNG\r\n\x1a\n" + b"\x00" * 20) def fake_save(obj, format=None): obj.write(buf.getvalue()) mock_pag.screenshot.return_value.save = MagicMock(side_effect=fake_save) with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_screenshot(log_path=tmp_log) assert result["ok"] is True assert result["image_b64"] is not None assert result["saved_to"] is None assert result["error"] is None def test_saves_to_path(self, tmp_log, tmp_path): mock_pag = _make_mock_pag() out_png = tmp_path / "shot.png" with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_screenshot(save_path=str(out_png), log_path=tmp_log) assert result["ok"] is True assert result["saved_to"] == str(out_png) assert result["image_b64"] is None mock_pag.screenshot.return_value.save.assert_called_once_with(str(out_png)) def test_logs_action(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): computer_screenshot(log_path=tmp_log) entry = _last_log_entry(tmp_log) assert entry["action"] == "screenshot" assert "ts" in entry def test_returns_error_when_headless(self, tmp_log): with patch("nexus.computer_use._get_pyautogui", return_value=None): result = computer_screenshot(log_path=tmp_log) assert result["ok"] is False assert "unavailable" in result["error"] def test_handles_screenshot_exception(self, tmp_log): mock_pag = _make_mock_pag(screenshot_raises=RuntimeError("display error")) with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_screenshot(log_path=tmp_log) assert result["ok"] is False assert "display error" in result["error"] def test_image_b64_not_written_to_log(self, tmp_log): """The (potentially huge) base64 blob must NOT appear in the audit log.""" mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): computer_screenshot(log_path=tmp_log) raw = tmp_log.read_text() assert "image_b64" not in raw # --------------------------------------------------------------------------- # computer_click # --------------------------------------------------------------------------- class TestComputerClick: def test_left_click_succeeds(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_click(100, 200, log_path=tmp_log) assert result["ok"] is True mock_pag.click.assert_called_once_with(100, 200, button="left") def test_right_click_blocked_without_confirm(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_click(100, 200, button="right", log_path=tmp_log) assert result["ok"] is False assert "confirm=True" in result["error"] mock_pag.click.assert_not_called() def test_right_click_allowed_with_confirm(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_click(100, 200, button="right", confirm=True, log_path=tmp_log) assert result["ok"] is True mock_pag.click.assert_called_once_with(100, 200, button="right") def test_middle_click_blocked_without_confirm(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_click(50, 50, button="middle", log_path=tmp_log) assert result["ok"] is False def test_middle_click_allowed_with_confirm(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_click(50, 50, button="middle", confirm=True, log_path=tmp_log) assert result["ok"] is True def test_unknown_button_rejected(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_click(0, 0, button="turbo", log_path=tmp_log) assert result["ok"] is False assert "Unknown button" in result["error"] def test_logs_click_action(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): computer_click(10, 20, log_path=tmp_log) entry = _last_log_entry(tmp_log) assert entry["action"] == "click" assert entry["params"]["x"] == 10 assert entry["params"]["y"] == 20 def test_returns_error_when_headless(self, tmp_log): with patch("nexus.computer_use._get_pyautogui", return_value=None): result = computer_click(0, 0, log_path=tmp_log) assert result["ok"] is False def test_handles_click_exception(self, tmp_log): mock_pag = _make_mock_pag() mock_pag.click.side_effect = Exception("out of bounds") with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_click(99999, 99999, log_path=tmp_log) assert result["ok"] is False assert "out of bounds" in result["error"] # --------------------------------------------------------------------------- # computer_type # --------------------------------------------------------------------------- class TestComputerType: def test_plain_text_succeeds(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_type("hello world", log_path=tmp_log) assert result["ok"] is True mock_pag.typewrite.assert_called_once_with("hello world", interval=0.02) def test_sensitive_text_blocked_without_confirm(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_type("mypassword123", log_path=tmp_log) assert result["ok"] is False assert "confirm=True" in result["error"] mock_pag.typewrite.assert_not_called() def test_sensitive_text_allowed_with_confirm(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_type("mypassword123", confirm=True, log_path=tmp_log) assert result["ok"] is True def test_sensitive_keywords_all_blocked(self, tmp_log): mock_pag = _make_mock_pag() for keyword in _SENSITIVE_KEYWORDS: with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_type(f"my{keyword}value", log_path=tmp_log) assert result["ok"] is False, f"keyword {keyword!r} should be blocked" def test_text_not_logged(self, tmp_log): """Actual typed text must NOT appear in the audit log.""" mock_pag = _make_mock_pag() secret = "super_secret_value_xyz" with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): computer_type(secret, confirm=True, log_path=tmp_log) raw = tmp_log.read_text() assert secret not in raw def test_logs_length_not_content(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): computer_type("hello", log_path=tmp_log) entry = _last_log_entry(tmp_log) assert entry["params"]["length"] == 5 def test_returns_error_when_headless(self, tmp_log): with patch("nexus.computer_use._get_pyautogui", return_value=None): result = computer_type("abc", log_path=tmp_log) assert result["ok"] is False def test_handles_type_exception(self, tmp_log): mock_pag = _make_mock_pag() mock_pag.typewrite.side_effect = Exception("keyboard error") with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_type("hello", log_path=tmp_log) assert result["ok"] is False assert "keyboard error" in result["error"] # --------------------------------------------------------------------------- # computer_scroll # --------------------------------------------------------------------------- class TestComputerScroll: def test_scroll_up(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_scroll(400, 300, amount=5, log_path=tmp_log) assert result["ok"] is True mock_pag.scroll.assert_called_once_with(5, x=400, y=300) def test_scroll_down_negative(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_scroll(400, 300, amount=-3, log_path=tmp_log) assert result["ok"] is True mock_pag.scroll.assert_called_once_with(-3, x=400, y=300) def test_logs_scroll_action(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): computer_scroll(10, 20, amount=2, log_path=tmp_log) entry = _last_log_entry(tmp_log) assert entry["action"] == "scroll" assert entry["params"]["amount"] == 2 def test_returns_error_when_headless(self, tmp_log): with patch("nexus.computer_use._get_pyautogui", return_value=None): result = computer_scroll(0, 0, log_path=tmp_log) assert result["ok"] is False def test_handles_scroll_exception(self, tmp_log): mock_pag = _make_mock_pag() mock_pag.scroll.side_effect = Exception("scroll error") with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): result = computer_scroll(0, 0, log_path=tmp_log) assert result["ok"] is False # --------------------------------------------------------------------------- # read_action_log # --------------------------------------------------------------------------- class TestReadActionLog: def test_returns_empty_list_when_no_log(self, tmp_path): missing = tmp_path / "nonexistent.jsonl" assert read_action_log(log_path=missing) == [] def test_returns_recent_entries(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): computer_click(1, 1, log_path=tmp_log) computer_click(2, 2, log_path=tmp_log) computer_click(3, 3, log_path=tmp_log) entries = read_action_log(n=2, log_path=tmp_log) assert len(entries) == 2 def test_newest_first(self, tmp_log): mock_pag = _make_mock_pag() with patch("nexus.computer_use._get_pyautogui", return_value=mock_pag): computer_click(1, 1, log_path=tmp_log) computer_scroll(5, 5, log_path=tmp_log) entries = read_action_log(log_path=tmp_log) # Most recent action (scroll) should be first assert entries[0]["action"] == "scroll" assert entries[1]["action"] == "click" def test_skips_malformed_lines(self, tmp_log): tmp_log.parent.mkdir(parents=True, exist_ok=True) tmp_log.write_text('{"action": "click", "ts": "2026-01-01", "params": {}, "result": {}}\nNOT JSON\n') entries = read_action_log(log_path=tmp_log) assert len(entries) == 1