chore: import upstream snapshot with attribution
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"""Pytest configuration and shared fixtures for computer package tests.
|
||||
|
||||
This file contains shared fixtures and configuration for all computer tests.
|
||||
Following SRP: This file ONLY handles test setup/teardown.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_interface():
|
||||
"""Mock computer interface for testing.
|
||||
|
||||
Use this fixture to test Computer logic without real OS calls.
|
||||
"""
|
||||
interface = AsyncMock()
|
||||
interface.screenshot = AsyncMock(return_value=b"fake_screenshot")
|
||||
interface.left_click = AsyncMock()
|
||||
interface.right_click = AsyncMock()
|
||||
interface.middle_click = AsyncMock()
|
||||
interface.double_click = AsyncMock()
|
||||
interface.type = AsyncMock()
|
||||
interface.key = AsyncMock()
|
||||
interface.move_mouse = AsyncMock()
|
||||
interface.scroll = AsyncMock()
|
||||
interface.get_screen_size = AsyncMock(return_value=(1920, 1080))
|
||||
|
||||
return interface
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cloud_provider():
|
||||
"""Mock cloud provider for testing.
|
||||
|
||||
Use this fixture to test cloud provider logic without real API calls.
|
||||
"""
|
||||
provider = AsyncMock()
|
||||
provider.start = AsyncMock()
|
||||
provider.stop = AsyncMock()
|
||||
provider.get_status = AsyncMock(return_value="running")
|
||||
provider.execute_command = AsyncMock(return_value="command output")
|
||||
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_local_provider():
|
||||
"""Mock local provider for testing.
|
||||
|
||||
Use this fixture to test local provider logic without real VM operations.
|
||||
"""
|
||||
provider = AsyncMock()
|
||||
provider.start = AsyncMock()
|
||||
provider.stop = AsyncMock()
|
||||
provider.get_status = AsyncMock(return_value="running")
|
||||
provider.execute_command = AsyncMock(return_value="command output")
|
||||
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def disable_telemetry(monkeypatch):
|
||||
"""Disable telemetry for tests.
|
||||
|
||||
Use this fixture to ensure no telemetry is sent during tests.
|
||||
"""
|
||||
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", "false")
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Unit tests for Computer class.
|
||||
|
||||
This file tests ONLY the Computer class initialization and context manager.
|
||||
Following SRP: This file tests ONE class (Computer).
|
||||
All external dependencies (providers, interfaces) are mocked.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestComputerImport:
|
||||
"""Test Computer module imports (SRP: Only tests imports)."""
|
||||
|
||||
def test_computer_class_exists(self):
|
||||
"""Test that Computer class can be imported."""
|
||||
from computer import Computer
|
||||
|
||||
assert Computer is not None
|
||||
|
||||
def test_vm_provider_type_exists(self):
|
||||
"""Test that VMProviderType enum can be imported."""
|
||||
from computer import VMProviderType
|
||||
|
||||
assert VMProviderType is not None
|
||||
|
||||
|
||||
class TestComputerInitialization:
|
||||
"""Test Computer initialization (SRP: Only tests initialization)."""
|
||||
|
||||
def test_computer_class_can_be_imported(self, disable_telemetry):
|
||||
"""Test that Computer class can be imported without errors."""
|
||||
from computer import Computer
|
||||
|
||||
assert Computer is not None
|
||||
|
||||
def test_computer_has_required_methods(self, disable_telemetry):
|
||||
"""Test that Computer class has required methods."""
|
||||
from computer import Computer
|
||||
|
||||
assert hasattr(Computer, "__aenter__")
|
||||
assert hasattr(Computer, "__aexit__")
|
||||
|
||||
|
||||
class TestComputerContextManager:
|
||||
"""Test Computer context manager protocol (SRP: Only tests context manager)."""
|
||||
|
||||
def test_computer_is_async_context_manager(self, disable_telemetry):
|
||||
"""Test that Computer has async context manager methods."""
|
||||
from computer import Computer
|
||||
|
||||
assert hasattr(Computer, "__aenter__")
|
||||
assert hasattr(Computer, "__aexit__")
|
||||
assert callable(Computer.__aenter__)
|
||||
assert callable(Computer.__aexit__)
|
||||
|
||||
|
||||
class TestComputerInterface:
|
||||
"""Test Computer.interface property (SRP: Only tests interface access)."""
|
||||
|
||||
def test_computer_class_structure(self, disable_telemetry):
|
||||
"""Test that Computer class has expected structure."""
|
||||
from computer import Computer
|
||||
|
||||
# Verify Computer is a class
|
||||
assert isinstance(Computer, type)
|
||||
@@ -0,0 +1,478 @@
|
||||
from computer.helpers import generate_source_code
|
||||
|
||||
|
||||
class TestSimpleCases:
|
||||
"""Test simple dependency cases"""
|
||||
|
||||
def test_simple_function_no_dependencies(self):
|
||||
"""Test simple function with no dependencies"""
|
||||
|
||||
def simple_func():
|
||||
return 42
|
||||
|
||||
code = generate_source_code(simple_func)
|
||||
|
||||
assert "def simple_func():" in code
|
||||
assert "return 42" in code
|
||||
|
||||
def test_function_with_parameters(self):
|
||||
"""Test function with parameters"""
|
||||
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
||||
code = generate_source_code(add)
|
||||
|
||||
assert "def add(a, b):" in code
|
||||
assert "return a + b" in code
|
||||
|
||||
def test_function_with_docstring(self):
|
||||
"""Test function with docstring"""
|
||||
|
||||
def documented():
|
||||
"""This is a docstring"""
|
||||
return True
|
||||
|
||||
code = generate_source_code(documented)
|
||||
|
||||
assert "def documented():" in code
|
||||
assert "This is a docstring" in code
|
||||
|
||||
|
||||
class TestImports:
|
||||
"""Test import handling"""
|
||||
|
||||
def test_stdlib_import_inside_function(self):
|
||||
"""Test function with stdlib import inside"""
|
||||
|
||||
def with_stdlib():
|
||||
import math
|
||||
|
||||
return math.sqrt(16)
|
||||
|
||||
code = generate_source_code(with_stdlib)
|
||||
|
||||
assert "import math" in code
|
||||
assert "def with_stdlib():" in code
|
||||
assert "math.sqrt(16)" in code
|
||||
|
||||
def test_multiple_stdlib_imports(self):
|
||||
"""Test function with multiple stdlib imports"""
|
||||
|
||||
def with_multiple():
|
||||
import json
|
||||
import math
|
||||
|
||||
return json.dumps({"value": math.pi})
|
||||
|
||||
code = generate_source_code(with_multiple)
|
||||
|
||||
assert "import json" in code
|
||||
assert "import math" in code
|
||||
|
||||
def test_from_import_stdlib(self):
|
||||
"""Test from X import Y style"""
|
||||
|
||||
def with_from_import():
|
||||
from math import pi, sqrt
|
||||
|
||||
return sqrt(16) + pi
|
||||
|
||||
code = generate_source_code(with_from_import)
|
||||
|
||||
assert "from math import" in code
|
||||
assert "sqrt, pi" in code or ("sqrt" in code and "pi" in code)
|
||||
|
||||
def test_third_party_global_import(self):
|
||||
"""Test globally imported third-party module"""
|
||||
from requests import get
|
||||
|
||||
def with_requests():
|
||||
return get
|
||||
|
||||
code = generate_source_code(with_requests)
|
||||
|
||||
assert "from requests import get" in code
|
||||
assert "def with_requests():" in code
|
||||
|
||||
|
||||
class TestHelperFunctions:
|
||||
"""Test helper function dependencies"""
|
||||
|
||||
def test_single_helper_function(self):
|
||||
"""Test function with one helper"""
|
||||
|
||||
def helper(x):
|
||||
return x * 2
|
||||
|
||||
def main():
|
||||
return helper(21)
|
||||
|
||||
code = generate_source_code(main)
|
||||
|
||||
assert "def helper(x):" in code
|
||||
assert "return x * 2" in code
|
||||
assert "def main():" in code
|
||||
assert "return helper(21)" in code
|
||||
|
||||
def test_nested_helper_functions(self):
|
||||
"""Test function with nested helpers"""
|
||||
|
||||
def helper_a(x):
|
||||
return x + 1
|
||||
|
||||
def helper_b(x):
|
||||
return helper_a(x) * 2
|
||||
|
||||
def main():
|
||||
return helper_b(10)
|
||||
|
||||
code = generate_source_code(main)
|
||||
|
||||
assert "def helper_a(x):" in code
|
||||
assert "def helper_b(x):" in code
|
||||
assert "def main():" in code
|
||||
|
||||
def test_helper_dependency_ordering(self):
|
||||
"""Test that helpers are ordered correctly (dependencies first)"""
|
||||
|
||||
def level_3(x):
|
||||
return x + 1
|
||||
|
||||
def level_2(x):
|
||||
return level_3(x) * 2
|
||||
|
||||
def level_1(x):
|
||||
return level_2(x) + 5
|
||||
|
||||
code = generate_source_code(level_1)
|
||||
|
||||
# Check ordering
|
||||
pos_3 = code.find("def level_3")
|
||||
pos_2 = code.find("def level_2")
|
||||
pos_1 = code.find("def level_1")
|
||||
|
||||
assert pos_3 < pos_2 < pos_1, "Functions not in correct dependency order"
|
||||
|
||||
def test_helper_with_import(self):
|
||||
"""Test helper function that uses imports"""
|
||||
import math
|
||||
|
||||
def helper(x):
|
||||
return math.sqrt(x)
|
||||
|
||||
def main():
|
||||
return helper(16)
|
||||
|
||||
code = generate_source_code(main)
|
||||
|
||||
assert "import math" in code
|
||||
assert "def helper(x):" in code
|
||||
assert "def main():" in code
|
||||
|
||||
|
||||
class TestGlobalConstants:
|
||||
"""Test global constant handling"""
|
||||
|
||||
def test_simple_constant(self):
|
||||
"""Test function using simple constant"""
|
||||
MY_CONSTANT = 42
|
||||
|
||||
def with_constant():
|
||||
return MY_CONSTANT * 2
|
||||
|
||||
code = generate_source_code(with_constant)
|
||||
|
||||
assert "MY_CONSTANT = 42" in code
|
||||
assert "def with_constant():" in code
|
||||
|
||||
def test_string_constant(self):
|
||||
"""Test function using string constant"""
|
||||
API_URL = "https://api.example.com"
|
||||
|
||||
def with_string():
|
||||
return API_URL
|
||||
|
||||
code = generate_source_code(with_string)
|
||||
|
||||
assert "API_URL" in code
|
||||
assert "https://api.example.com" in code
|
||||
|
||||
def test_multiple_constants(self):
|
||||
"""Test function using multiple constants"""
|
||||
BASE_URL = "https://example.com"
|
||||
TIMEOUT = 30
|
||||
|
||||
def with_multiple():
|
||||
return f"{BASE_URL}?timeout={TIMEOUT}"
|
||||
|
||||
code = generate_source_code(with_multiple)
|
||||
|
||||
assert "BASE_URL" in code
|
||||
assert "TIMEOUT" in code
|
||||
|
||||
|
||||
class TestClassDefinitions:
|
||||
"""Test class definition handling"""
|
||||
|
||||
def test_simple_class(self):
|
||||
"""Test function using simple class"""
|
||||
|
||||
class Calculator:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def add(self, x):
|
||||
return self.value + x
|
||||
|
||||
def with_class():
|
||||
calc = Calculator(10)
|
||||
return calc.add(5)
|
||||
|
||||
code = generate_source_code(with_class)
|
||||
|
||||
assert "class Calculator:" in code
|
||||
assert "__init__" in code
|
||||
assert "def add" in code
|
||||
assert "def with_class():" in code
|
||||
|
||||
def test_class_with_methods(self):
|
||||
"""Test class with multiple methods"""
|
||||
|
||||
class Math:
|
||||
def add(self, a, b):
|
||||
return a + b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
def use_math():
|
||||
m = Math()
|
||||
return m.add(2, 3) + m.multiply(4, 5)
|
||||
|
||||
code = generate_source_code(use_math)
|
||||
|
||||
assert "class Math:" in code
|
||||
assert "def add" in code
|
||||
assert "def multiply" in code
|
||||
|
||||
|
||||
class TestDecoratorRemoval:
|
||||
"""Test decorator removal"""
|
||||
|
||||
def test_simple_decorator_removed(self):
|
||||
"""Test that decorators are removed"""
|
||||
|
||||
def my_decorator(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
@my_decorator
|
||||
def decorated():
|
||||
return 42
|
||||
|
||||
code = generate_source_code(decorated)
|
||||
|
||||
assert "@my_decorator" not in code
|
||||
assert "def decorated():" in code
|
||||
assert "return 42" in code
|
||||
|
||||
def test_multiple_decorators_removed(self):
|
||||
"""Test that multiple decorators are removed"""
|
||||
|
||||
def decorator1(f):
|
||||
return f
|
||||
|
||||
def decorator2(f):
|
||||
return f
|
||||
|
||||
@decorator1
|
||||
@decorator2
|
||||
def multi_decorated():
|
||||
return True
|
||||
|
||||
code = generate_source_code(multi_decorated)
|
||||
|
||||
assert "@decorator1" not in code
|
||||
assert "@decorator2" not in code
|
||||
assert "def multi_decorated():" in code
|
||||
|
||||
|
||||
class TestComplexScenarios:
|
||||
"""Test complex real-world scenarios"""
|
||||
|
||||
def test_api_client_pattern(self):
|
||||
"""Test typical API client pattern"""
|
||||
import json
|
||||
|
||||
BASE_URL = "https://api.example.com"
|
||||
|
||||
def make_request(endpoint):
|
||||
return f"{BASE_URL}/{endpoint}"
|
||||
|
||||
def parse_response(response):
|
||||
return json.loads(response)
|
||||
|
||||
def get_user(user_id):
|
||||
url = make_request(f"users/{user_id}")
|
||||
response = f'{{"id": {user_id}}}'
|
||||
return parse_response(response)
|
||||
|
||||
code = generate_source_code(get_user)
|
||||
|
||||
assert "import json" in code
|
||||
assert "BASE_URL" in code
|
||||
assert "def make_request" in code
|
||||
assert "def parse_response" in code
|
||||
assert "def get_user" in code
|
||||
|
||||
def test_data_processing_pipeline(self):
|
||||
"""Test data processing pipeline"""
|
||||
|
||||
def validate_data(data):
|
||||
return [x for x in data if x > 0]
|
||||
|
||||
def transform_data(data):
|
||||
return [x * 2 for x in data]
|
||||
|
||||
def process_pipeline(raw_data):
|
||||
valid = validate_data(raw_data)
|
||||
return transform_data(valid)
|
||||
|
||||
code = generate_source_code(process_pipeline)
|
||||
|
||||
assert "def validate_data" in code
|
||||
assert "def transform_data" in code
|
||||
assert "def process_pipeline" in code
|
||||
|
||||
# Check ordering
|
||||
validate_pos = code.find("def validate_data")
|
||||
transform_pos = code.find("def transform_data")
|
||||
pipeline_pos = code.find("def process_pipeline")
|
||||
|
||||
assert validate_pos < pipeline_pos
|
||||
assert transform_pos < pipeline_pos
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Test edge cases"""
|
||||
|
||||
def test_function_with_default_args(self):
|
||||
"""Test function with default arguments"""
|
||||
|
||||
def with_defaults(a, b=10, c=20):
|
||||
return a + b + c
|
||||
|
||||
code = generate_source_code(with_defaults)
|
||||
|
||||
assert "def with_defaults(a, b=10, c=20):" in code
|
||||
|
||||
def test_function_with_kwargs(self):
|
||||
"""Test function with *args and **kwargs"""
|
||||
|
||||
def with_varargs(*args, **kwargs):
|
||||
return sum(args)
|
||||
|
||||
code = generate_source_code(with_varargs)
|
||||
|
||||
assert "def with_varargs(*args, **kwargs):" in code
|
||||
|
||||
def test_async_function(self):
|
||||
"""Test async function"""
|
||||
|
||||
async def async_func():
|
||||
return 42
|
||||
|
||||
code = generate_source_code(async_func)
|
||||
|
||||
assert "async def async_func():" in code
|
||||
assert "return 42" in code
|
||||
|
||||
def test_lambda_works_in_files(self):
|
||||
"""Test that lambda functions work when defined in files"""
|
||||
lambda_func = lambda x: x * 2
|
||||
|
||||
# Lambda functions defined in files CAN have their source extracted
|
||||
code = generate_source_code(lambda_func)
|
||||
|
||||
# Should include the lambda
|
||||
assert "lambda" in code.lower() or "lambda_func" in code
|
||||
|
||||
|
||||
class TestCaching:
|
||||
"""Test caching mechanism"""
|
||||
|
||||
def test_cache_returns_same_result(self):
|
||||
"""Test that cache returns identical results"""
|
||||
|
||||
def cached_func():
|
||||
return 42
|
||||
|
||||
code1 = generate_source_code(cached_func)
|
||||
code2 = generate_source_code(cached_func)
|
||||
|
||||
assert code1 == code2
|
||||
|
||||
def test_different_functions_different_cache(self):
|
||||
"""Test that different functions have different cache entries"""
|
||||
|
||||
def func1():
|
||||
return 1
|
||||
|
||||
def func2():
|
||||
return 2
|
||||
|
||||
code1 = generate_source_code(func1)
|
||||
code2 = generate_source_code(func2)
|
||||
|
||||
assert code1 != code2
|
||||
assert "return 1" in code1
|
||||
assert "return 2" in code2
|
||||
|
||||
|
||||
class TestBuiltinHandling:
|
||||
"""Test that builtins are handled correctly"""
|
||||
|
||||
def test_builtin_not_included(self):
|
||||
"""Test that builtin functions are not included as dependencies"""
|
||||
|
||||
def uses_builtins():
|
||||
data = list(range(10))
|
||||
return len(data)
|
||||
|
||||
code = generate_source_code(uses_builtins)
|
||||
|
||||
# Should not include definitions for list, range, len
|
||||
assert "def list" not in code
|
||||
assert "def range" not in code
|
||||
assert "def len" not in code
|
||||
assert "def uses_builtins():" in code
|
||||
|
||||
|
||||
class TestImportStylePreservation:
|
||||
"""Test that import styles are preserved"""
|
||||
|
||||
def test_from_import_preserved(self):
|
||||
"""Test from X import Y is preserved"""
|
||||
from math import sqrt
|
||||
|
||||
def use_sqrt():
|
||||
return sqrt(16)
|
||||
|
||||
code = generate_source_code(use_sqrt)
|
||||
|
||||
assert "from math import sqrt" in code
|
||||
|
||||
def test_import_as_preserved(self):
|
||||
"""Test import X as Y is preserved"""
|
||||
import json as j
|
||||
|
||||
def use_json():
|
||||
return j.dumps({"key": "value"})
|
||||
|
||||
code = generate_source_code(use_json)
|
||||
|
||||
# The import style should be preserved
|
||||
assert "import json" in code
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Unit tests for direct-proxy support in the computer interface.
|
||||
|
||||
These tests verify that passing an explicit ``api_base_url`` (plus arbitrary
|
||||
``api_headers``) makes the interface derive its REST/WebSocket/Playwright URLs
|
||||
from that base URL -- preserving scheme, host, port and path prefix -- and sends
|
||||
the extra headers. This is what allows connecting directly to a computer-server
|
||||
behind an authenticated, path-prefixed reverse proxy (e.g. cyclops-cs) without a
|
||||
localhost forwarder.
|
||||
|
||||
Following SRP: This file tests ONLY URL building and header propagation for the
|
||||
interface layer.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from computer.interface.factory import InterfaceFactory
|
||||
from computer.interface.windows import WindowsComputerInterface
|
||||
|
||||
PROXY_BASE = "https://run.cua.ai/api/svc/ns/sb-api"
|
||||
|
||||
|
||||
class TestApiBaseUrlBuilding:
|
||||
"""api_base_url should fully drive REST/WS/Playwright URLs."""
|
||||
|
||||
def test_rest_uri_uses_base_url(self):
|
||||
iface = WindowsComputerInterface(
|
||||
"ignored-ip", api_base_url=PROXY_BASE, api_headers={"Authorization": "Bearer t"}
|
||||
)
|
||||
assert iface.rest_uri == f"{PROXY_BASE}/cmd"
|
||||
|
||||
def test_ws_uri_swaps_scheme_and_keeps_prefix(self):
|
||||
iface = WindowsComputerInterface("ignored-ip", api_base_url=PROXY_BASE)
|
||||
assert iface.ws_uri == "wss://run.cua.ai/api/svc/ns/sb-api/ws"
|
||||
|
||||
def test_trailing_slash_is_stripped(self):
|
||||
iface = WindowsComputerInterface("ignored-ip", api_base_url=PROXY_BASE + "/")
|
||||
assert iface.rest_uri == f"{PROXY_BASE}/cmd"
|
||||
assert iface.ws_uri == "wss://run.cua.ai/api/svc/ns/sb-api/ws"
|
||||
|
||||
def test_http_base_maps_to_ws(self):
|
||||
iface = WindowsComputerInterface("ignored-ip", api_base_url="http://localhost:9999/prefix")
|
||||
assert iface.rest_uri == "http://localhost:9999/prefix/cmd"
|
||||
assert iface.ws_uri == "ws://localhost:9999/prefix/ws"
|
||||
|
||||
def test_api_headers_stored(self):
|
||||
headers = {"Authorization": "Bearer secret"}
|
||||
iface = WindowsComputerInterface("ignored-ip", api_base_url=PROXY_BASE, api_headers=headers)
|
||||
assert iface._api_headers == headers
|
||||
|
||||
def test_factory_threads_params(self):
|
||||
iface = InterfaceFactory.create_interface_for_os(
|
||||
os="windows",
|
||||
ip_address="ignored-ip",
|
||||
api_base_url=PROXY_BASE,
|
||||
api_headers={"Authorization": "Bearer t"},
|
||||
)
|
||||
assert iface.rest_uri == f"{PROXY_BASE}/cmd"
|
||||
assert iface.ws_uri == "wss://run.cua.ai/api/svc/ns/sb-api/ws"
|
||||
assert iface._api_headers == {"Authorization": "Bearer t"}
|
||||
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""Without api_base_url, behavior is unchanged (ip_address + port)."""
|
||||
|
||||
def test_no_base_url_local(self):
|
||||
iface = WindowsComputerInterface("192.168.1.5")
|
||||
assert iface.rest_uri == "http://192.168.1.5:8000/cmd"
|
||||
assert iface.ws_uri == "ws://192.168.1.5:8000/ws"
|
||||
assert iface._api_headers == {}
|
||||
|
||||
def test_no_base_url_with_api_key(self):
|
||||
iface = WindowsComputerInterface("192.168.1.5", api_key="abc", vm_name="vm")
|
||||
assert iface.rest_uri == "https://192.168.1.5:8443/cmd"
|
||||
assert iface.ws_uri == "wss://192.168.1.5:8443/ws"
|
||||
|
||||
def test_no_base_url_custom_port(self):
|
||||
iface = WindowsComputerInterface("192.168.1.5", api_port=1234)
|
||||
assert iface.rest_uri == "http://192.168.1.5:1234/cmd"
|
||||
assert iface.ws_uri == "ws://192.168.1.5:1234/ws"
|
||||
|
||||
|
||||
class TestRestHeaderMerging:
|
||||
"""api_headers must be merged into REST requests."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_command_rest_sends_headers(self, monkeypatch):
|
||||
captured = {}
|
||||
|
||||
class FakeResp:
|
||||
status = 200
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
async def text(self):
|
||||
return 'data: {"success": true}'
|
||||
|
||||
class FakeSession:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
def post(self, url, json=None, headers=None):
|
||||
captured["url"] = url
|
||||
captured["headers"] = headers
|
||||
return FakeResp()
|
||||
|
||||
import aiohttp
|
||||
|
||||
monkeypatch.setattr(aiohttp, "ClientSession", lambda *a, **k: FakeSession())
|
||||
|
||||
iface = WindowsComputerInterface(
|
||||
"ignored-ip",
|
||||
api_base_url=PROXY_BASE,
|
||||
api_headers={"Authorization": "Bearer secret"},
|
||||
)
|
||||
result = await iface._send_command_rest("version", {})
|
||||
assert result.get("success") is True
|
||||
assert captured["url"] == f"{PROXY_BASE}/cmd"
|
||||
assert captured["headers"]["Authorization"] == "Bearer secret"
|
||||
Reference in New Issue
Block a user