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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:19 +08:00
commit 91e75e620b
3227 changed files with 1307078 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Tests for cua-cli."""
@@ -0,0 +1 @@
"""API module tests."""
@@ -0,0 +1 @@
"""Auth module tests."""
@@ -0,0 +1,230 @@
"""Tests for auth.store module."""
import sqlite3
import threading
from unittest.mock import patch
import pytest
from cua_cli.auth.store import (
CredentialStore,
clear_credentials,
get_api_key,
require_api_key,
save_api_key,
)
class TestCredentialStore:
"""Tests for CredentialStore class."""
def test_init_creates_database(self, tmp_path):
"""Test that initializing creates the database file."""
db_path = tmp_path / "test.db"
CredentialStore(db_path)
assert db_path.exists()
def test_init_creates_parent_directory(self, tmp_path):
"""Test that initializing creates parent directories."""
db_path = tmp_path / "nested" / "dir" / "test.db"
CredentialStore(db_path)
assert db_path.parent.exists()
assert db_path.exists()
def test_set_and_get(self, tmp_path):
"""Test setting and getting a value."""
store = CredentialStore(tmp_path / "test.db")
store.set("test_key", "test_value")
result = store.get("test_key")
assert result == "test_value"
def test_get_nonexistent_key(self, tmp_path):
"""Test getting a nonexistent key returns None."""
store = CredentialStore(tmp_path / "test.db")
result = store.get("nonexistent")
assert result is None
def test_set_overwrites_existing(self, tmp_path):
"""Test that setting an existing key overwrites the value."""
store = CredentialStore(tmp_path / "test.db")
store.set("key", "value1")
store.set("key", "value2")
result = store.get("key")
assert result == "value2"
def test_delete_existing_key(self, tmp_path):
"""Test deleting an existing key returns True."""
store = CredentialStore(tmp_path / "test.db")
store.set("key", "value")
result = store.delete("key")
assert result is True
assert store.get("key") is None
def test_delete_nonexistent_key(self, tmp_path):
"""Test deleting a nonexistent key returns False."""
store = CredentialStore(tmp_path / "test.db")
result = store.delete("nonexistent")
assert result is False
def test_clear_removes_all(self, tmp_path):
"""Test that clear removes all values."""
store = CredentialStore(tmp_path / "test.db")
store.set("key1", "value1")
store.set("key2", "value2")
store.clear()
assert store.get("key1") is None
assert store.get("key2") is None
def test_wal_mode_enabled(self, tmp_path):
"""Test that WAL mode is enabled for concurrent access."""
db_path = tmp_path / "test.db"
CredentialStore(db_path)
conn = sqlite3.connect(db_path)
try:
cursor = conn.execute("PRAGMA journal_mode")
mode = cursor.fetchone()[0]
assert mode.lower() == "wal"
finally:
conn.close()
def test_concurrent_access(self, tmp_path):
"""Test that concurrent access works with WAL mode."""
db_path = tmp_path / "test.db"
store = CredentialStore(db_path)
results = []
errors = []
def writer():
try:
for i in range(10):
store.set(f"key_{i}", f"value_{i}")
except Exception as e:
errors.append(e)
def reader():
try:
for i in range(10):
result = store.get(f"key_{i}")
if result is not None:
results.append(result)
except Exception as e:
errors.append(e)
threads = [
threading.Thread(target=writer),
threading.Thread(target=reader),
threading.Thread(target=reader),
]
for t in threads:
t.start()
for t in threads:
t.join()
assert len(errors) == 0
class TestGetApiKey:
"""Tests for get_api_key function."""
def test_returns_env_var_first(self, monkeypatch, tmp_path):
"""Test that environment variable takes precedence."""
# Set up a store with a different value
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
with patch("cua_cli.auth.store._store", None):
monkeypatch.setenv("CUA_API_KEY", "env-key")
save_api_key("stored-key")
result = get_api_key()
assert result == "env-key"
def test_falls_back_to_stored(self, monkeypatch, tmp_path):
"""Test that stored key is used when env var not set."""
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
with patch("cua_cli.auth.store._store", None):
monkeypatch.delenv("CUA_API_KEY", raising=False)
save_api_key("stored-key")
result = get_api_key()
assert result == "stored-key"
def test_returns_none_when_not_found(self, monkeypatch, tmp_path):
"""Test that None is returned when no key is found."""
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
with patch("cua_cli.auth.store._store", None):
monkeypatch.delenv("CUA_API_KEY", raising=False)
result = get_api_key()
assert result is None
class TestSaveApiKey:
"""Tests for save_api_key function."""
def test_saves_api_key(self, tmp_path, monkeypatch):
"""Test that API key is saved correctly."""
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
with patch("cua_cli.auth.store._store", None):
monkeypatch.delenv("CUA_API_KEY", raising=False)
save_api_key("my-api-key")
result = get_api_key()
assert result == "my-api-key"
class TestClearCredentials:
"""Tests for clear_credentials function."""
def test_clears_all_credentials(self, tmp_path, monkeypatch):
"""Test that all credentials are cleared."""
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
with patch("cua_cli.auth.store._store", None):
monkeypatch.delenv("CUA_API_KEY", raising=False)
save_api_key("my-api-key")
clear_credentials()
result = get_api_key()
assert result is None
class TestRequireApiKey:
"""Tests for require_api_key function."""
def test_returns_key_when_available(self, monkeypatch):
"""Test that key is returned when available."""
monkeypatch.setenv("CUA_API_KEY", "my-api-key")
result = require_api_key()
assert result == "my-api-key"
def test_raises_when_not_available(self, monkeypatch, tmp_path):
"""Test that RuntimeError is raised when no key available."""
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
with patch("cua_cli.auth.store._store", None):
monkeypatch.delenv("CUA_API_KEY", raising=False)
with pytest.raises(RuntimeError) as exc_info:
require_api_key()
assert "No API key configured" in str(exc_info.value)
assert "cua auth login" in str(exc_info.value)
@@ -0,0 +1 @@
"""Command module tests."""
@@ -0,0 +1,200 @@
"""Tests for auth command module."""
import argparse
from unittest.mock import patch
from cua_cli.commands import auth
class TestRegisterParser:
"""Tests for register_parser function."""
def test_registers_auth_command(self):
"""Test that auth command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
auth.register_parser(subparsers)
args = parser.parse_args(["auth", "login"])
assert args.auth_command == "login"
def test_login_has_api_key_flag(self):
"""Test that login has --api-key flag."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
auth.register_parser(subparsers)
args = parser.parse_args(["auth", "login", "--api-key", "my-key"])
assert args.api_key == "my-key"
def test_logout_command(self):
"""Test that logout command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
auth.register_parser(subparsers)
args = parser.parse_args(["auth", "logout"])
assert args.auth_command == "logout"
def test_env_command(self):
"""Test that env command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
auth.register_parser(subparsers)
args = parser.parse_args(["auth", "env"])
assert args.auth_command == "env"
class TestExecute:
"""Tests for execute function."""
def test_dispatch_to_login(self, args_namespace):
"""Test dispatch to login command."""
args = args_namespace(command="auth", auth_command="login", api_key=False)
with patch.object(auth, "cmd_login", return_value=0) as mock_cmd:
result = auth.execute(args)
mock_cmd.assert_called_once_with(args)
assert result == 0
def test_dispatch_to_logout(self, args_namespace):
"""Test dispatch to logout command."""
args = args_namespace(command="auth", auth_command="logout")
with patch.object(auth, "cmd_logout", return_value=0) as mock_cmd:
auth.execute(args)
mock_cmd.assert_called_once_with(args)
def test_dispatch_to_env(self, args_namespace):
"""Test dispatch to env command."""
args = args_namespace(command="auth", auth_command="env")
with patch.object(auth, "cmd_env", return_value=0) as mock_cmd:
auth.execute(args)
mock_cmd.assert_called_once_with(args)
def test_unknown_command_returns_error(self, args_namespace):
"""Test that unknown command returns error."""
args = args_namespace(command="auth", auth_command=None)
with patch.object(auth, "print_error"):
result = auth.execute(args)
assert result == 1
class TestCmdLogin:
"""Tests for cmd_login function."""
def test_login_with_api_key_direct(self, args_namespace):
"""Test login with --api-key flag and value."""
args = args_namespace(api_key="test-api-key")
with patch.object(auth, "get_api_key", return_value=None):
with patch.object(auth, "save_api_key") as mock_save:
with patch.object(auth, "print_info"):
with patch.object(auth, "print_success"):
result = auth.cmd_login(args)
mock_save.assert_called_once_with("test-api-key")
assert result == 0
def test_login_already_authenticated(self, args_namespace):
"""Test login when already authenticated."""
args = args_namespace(api_key=None)
with patch.object(auth, "get_api_key", return_value="existing-key"):
with patch.object(auth, "print_info") as mock_info:
result = auth.cmd_login(args)
assert result == 0
mock_info.assert_called()
def test_login_browser_flow(self, args_namespace):
"""Test login with browser OAuth flow."""
args = args_namespace(api_key=None)
with patch.object(auth, "get_api_key", return_value=None):
with patch.object(auth, "run_async") as mock_run:
mock_run.return_value = "browser-api-key"
with patch.object(auth, "save_api_key") as mock_save:
with patch.object(auth, "print_success"):
result = auth.cmd_login(args)
mock_save.assert_called_once_with("browser-api-key")
assert result == 0
def test_login_browser_flow_timeout(self, args_namespace):
"""Test login when browser flow times out."""
args = args_namespace(api_key=None)
with patch.object(auth, "get_api_key", return_value=None):
with patch.object(auth, "run_async") as mock_run:
mock_run.side_effect = TimeoutError("Authentication timed out")
with patch.object(auth, "print_error"):
result = auth.cmd_login(args)
assert result == 1
class TestCmdLogout:
"""Tests for cmd_logout function."""
def test_logout_clears_credentials(self, args_namespace):
"""Test that logout clears credentials."""
args = args_namespace()
with patch.object(auth, "clear_credentials") as mock_clear:
with patch.object(auth, "print_success"):
result = auth.cmd_logout(args)
mock_clear.assert_called_once()
assert result == 0
class TestCmdEnv:
"""Tests for cmd_env function."""
def test_env_writes_to_dotenv(self, args_namespace, tmp_path, monkeypatch):
"""Test that env command writes to .env file."""
args = args_namespace(file=str(tmp_path / ".env"))
with patch.object(auth, "get_api_key", return_value="test-api-key"):
with patch.object(auth, "print_success"):
result = auth.cmd_env(args)
assert result == 0
env_file = tmp_path / ".env"
assert env_file.exists()
content = env_file.read_text()
assert "CUA_API_KEY=test-api-key" in content
def test_env_no_credentials_fails(self, args_namespace, tmp_path):
"""Test that env command fails when not logged in."""
args = args_namespace(file=str(tmp_path / ".env"))
with patch.object(auth, "get_api_key", return_value=None):
with patch.object(auth, "print_error"):
result = auth.cmd_env(args)
assert result == 1
def test_env_appends_to_existing_dotenv(self, args_namespace, tmp_path):
"""Test that env command appends to existing .env file."""
env_file = tmp_path / ".env"
env_file.write_text("EXISTING_VAR=value\n")
args = args_namespace(file=str(env_file))
with patch.object(auth, "get_api_key", return_value="test-api-key"):
with patch.object(auth, "print_success"):
result = auth.cmd_env(args)
assert result == 0
content = env_file.read_text()
assert "EXISTING_VAR=value" in content
assert "CUA_API_KEY=test-api-key" in content
@@ -0,0 +1,297 @@
"""Tests for image command module."""
import argparse
from unittest.mock import patch
from cua_cli.commands import image
class TestRegisterParser:
"""Tests for register_parser function."""
def test_registers_image_command(self):
"""Test that image command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
image.register_parser(subparsers)
args = parser.parse_args(["image", "list"])
assert args.image_command == "list"
def test_registers_img_alias(self):
"""Test that img alias is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
image.register_parser(subparsers)
args = parser.parse_args(["img", "list"])
assert args.image_command == "list"
def test_list_has_json_flag(self):
"""Test that list command has --json flag."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
image.register_parser(subparsers)
args = parser.parse_args(["image", "list", "--json"])
assert args.json is True
def test_list_has_local_flag(self):
"""Test that list command has --local flag."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
image.register_parser(subparsers)
args = parser.parse_args(["image", "list", "--local"])
assert args.local is True
def test_push_command(self):
"""Test that push command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
image.register_parser(subparsers)
args = parser.parse_args(["image", "push", "my-image"])
assert args.image_command == "push"
assert args.name == "my-image"
def test_pull_command(self):
"""Test that pull command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
image.register_parser(subparsers)
args = parser.parse_args(["image", "pull", "my-image"])
assert args.image_command == "pull"
assert args.name == "my-image"
def test_delete_command(self):
"""Test that delete command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
image.register_parser(subparsers)
args = parser.parse_args(["image", "delete", "my-image", "--force"])
assert args.image_command == "delete"
assert args.name == "my-image"
assert args.force is True
class TestExecute:
"""Tests for execute function."""
def test_dispatch_to_list(self, args_namespace):
"""Test dispatch to list command."""
args = args_namespace(
command="image",
image_command="list",
json=False,
local=False,
cloud=False,
)
with patch.object(image, "cmd_list", return_value=0) as mock_cmd:
result = image.execute(args)
mock_cmd.assert_called_once_with(args)
assert result == 0
def test_dispatch_to_push(self, args_namespace):
"""Test dispatch to push command."""
args = args_namespace(command="image", image_command="push", name="my-image")
with patch.object(image, "cmd_push", return_value=0) as mock_cmd:
image.execute(args)
mock_cmd.assert_called_once_with(args)
def test_dispatch_to_pull(self, args_namespace):
"""Test dispatch to pull command."""
args = args_namespace(command="image", image_command="pull", name="my-image")
with patch.object(image, "cmd_pull", return_value=0) as mock_cmd:
image.execute(args)
mock_cmd.assert_called_once_with(args)
def test_dispatch_to_delete(self, args_namespace):
"""Test dispatch to delete command."""
args = args_namespace(command="image", image_command="delete", name="my-image")
with patch.object(image, "cmd_delete", return_value=0) as mock_cmd:
image.execute(args)
mock_cmd.assert_called_once_with(args)
def test_unknown_command_returns_error(self, args_namespace):
"""Test that unknown command returns error."""
args = args_namespace(command="image", image_command=None)
with patch.object(image, "print_error"):
result = image.execute(args)
assert result == 1
class TestCmdList:
"""Tests for cmd_list function."""
def test_list_cloud_images(self, args_namespace, sample_cloud_images, mock_api_key):
"""Test listing cloud images."""
args = args_namespace(json=False, local=False, cloud=True)
with patch.object(image, "run_async") as mock_run:
mock_run.return_value = [
{
"name": "ubuntu-22.04",
"type": "qcow2",
"tag": "latest",
"size": "5.0 GB",
"status": "ready",
"created": "2024-01-10",
"source": "cloud",
}
]
with patch.object(image, "print_table") as mock_print:
result = image.cmd_list(args)
assert result == 0
mock_print.assert_called_once()
def test_list_local_images(self, args_namespace, tmp_path, monkeypatch):
"""Test listing local images delegates to local_image."""
args = args_namespace(json=False, local=True, cloud=False)
with patch("cua_cli.commands.local_image.cmd_local_list", return_value=0) as mock_local:
result = image.cmd_list(args)
assert result == 0
mock_local.assert_called_once_with(args)
def test_list_empty(self, args_namespace, tmp_path, monkeypatch):
"""Test listing when no local images exist."""
args = args_namespace(json=False, local=True, cloud=False)
with patch("cua_cli.commands.local_image.cmd_local_list", return_value=0) as mock_local:
result = image.cmd_list(args)
assert result == 0
mock_local.assert_called_once()
def test_list_json_output(self, args_namespace, mock_api_key):
"""Test JSON output format."""
args = args_namespace(json=True, local=False, cloud=True)
with patch.object(image, "run_async") as mock_run:
mock_run.return_value = [{"name": "test", "source": "cloud"}]
with patch.object(image, "print_json") as mock_print:
result = image.cmd_list(args)
assert result == 0
mock_print.assert_called_once()
class TestCmdPush:
"""Tests for cmd_push function."""
def test_push_success(self, args_namespace, temp_image_file, mock_api_key):
"""Test successful image push."""
args = args_namespace(
name="test-image",
tag="latest",
type="qcow2",
file=str(temp_image_file),
)
with patch.object(image, "run_async") as mock_run:
mock_run.return_value = 0
result = image.cmd_push(args)
assert result == 0
def test_push_file_not_found(self, args_namespace, mock_api_key):
"""Test push with nonexistent file."""
args = args_namespace(
name="test-image",
tag="latest",
type="qcow2",
file="/nonexistent/path",
)
with patch.object(image, "print_error") as mock_error:
result = image.cmd_push(args)
assert result == 1
mock_error.assert_called()
class TestCmdPull:
"""Tests for cmd_pull function."""
def test_pull_success(self, args_namespace, tmp_path, mock_api_key):
"""Test successful image pull."""
args = args_namespace(
name="test-image",
tag="latest",
output=str(tmp_path / "output.qcow2"),
)
with patch.object(image, "run_async") as mock_run:
mock_run.return_value = 0
result = image.cmd_pull(args)
assert result == 0
class TestCmdDelete:
"""Tests for cmd_delete function."""
def test_delete_with_force(self, args_namespace, mock_api_key):
"""Test delete with --force flag."""
args = args_namespace(name="test-image", tag="latest", force=True, local=False)
with patch.object(image, "run_async") as mock_run:
mock_run.return_value = 0
result = image.cmd_delete(args)
assert result == 0
def test_delete_without_force_prompts(self, args_namespace, mock_api_key):
"""Test delete without --force shows prompt."""
args = args_namespace(name="test-image", tag="latest", force=False, local=False)
with patch.object(image, "print_info") as mock_print:
result = image.cmd_delete(args)
assert result == 1
mock_print.assert_called()
def test_delete_local_delegates(self, args_namespace):
"""Test delete with --local delegates to local_image."""
args = args_namespace(name="test-image", tag="latest", force=True, local=True)
with patch("cua_cli.commands.local_image.cmd_local_delete", return_value=0) as mock_local:
result = image.cmd_delete(args)
assert result == 0
mock_local.assert_called_once_with(args)
class TestHelperFunctions:
"""Tests for helper functions."""
def test_format_date_valid(self):
"""Test formatting a valid ISO date."""
result = image._format_date("2024-01-15T10:30:00Z")
assert result == "2024-01-15"
def test_format_date_empty(self):
"""Test formatting empty string."""
result = image._format_date("")
assert result == "-"
def test_format_date_invalid(self):
"""Test formatting invalid date."""
result = image._format_date("not-a-date")
assert result == "not-a-date" # Falls back to first 10 chars
@@ -0,0 +1,232 @@
"""Tests for MCP server command module."""
import argparse
import sys
from unittest.mock import patch
from cua_cli.commands.mcp import (
PERMISSION_GROUPS,
Permission,
execute,
parse_permissions,
register_parser,
)
class TestPermission:
"""Tests for Permission enum."""
def test_all_permissions_have_values(self):
"""Test that all permissions have string values."""
for perm in Permission:
assert isinstance(perm.value, str)
assert ":" in perm.value
def test_sandbox_permissions(self):
"""Test sandbox permission values."""
assert Permission.SANDBOX_LIST.value == "sandbox:list"
assert Permission.SANDBOX_CREATE.value == "sandbox:create"
assert Permission.SANDBOX_DELETE.value == "sandbox:delete"
def test_computer_permissions(self):
"""Test computer permission values."""
assert Permission.COMPUTER_SCREENSHOT.value == "computer:screenshot"
assert Permission.COMPUTER_CLICK.value == "computer:click"
assert Permission.COMPUTER_TYPE.value == "computer:type"
def test_skills_permissions(self):
"""Test skills permission values."""
assert Permission.SKILLS_LIST.value == "skills:list"
assert Permission.SKILLS_READ.value == "skills:read"
assert Permission.SKILLS_RECORD.value == "skills:record"
class TestPermissionGroups:
"""Tests for permission groups."""
def test_sandbox_all_group(self):
"""Test sandbox:all group contains all sandbox permissions."""
group = PERMISSION_GROUPS["sandbox:all"]
assert Permission.SANDBOX_LIST in group
assert Permission.SANDBOX_CREATE in group
assert Permission.SANDBOX_DELETE in group
assert Permission.SANDBOX_START in group
assert Permission.SANDBOX_STOP in group
def test_sandbox_readonly_group(self):
"""Test sandbox:readonly group contains only read permissions."""
group = PERMISSION_GROUPS["sandbox:readonly"]
assert Permission.SANDBOX_LIST in group
assert Permission.SANDBOX_GET in group
assert Permission.SANDBOX_CREATE not in group
assert Permission.SANDBOX_DELETE not in group
def test_computer_all_group(self):
"""Test computer:all group contains all computer permissions."""
group = PERMISSION_GROUPS["computer:all"]
assert Permission.COMPUTER_SCREENSHOT in group
assert Permission.COMPUTER_CLICK in group
assert Permission.COMPUTER_TYPE in group
assert Permission.COMPUTER_SHELL in group
def test_computer_readonly_group(self):
"""Test computer:readonly group contains only screenshot."""
group = PERMISSION_GROUPS["computer:readonly"]
assert Permission.COMPUTER_SCREENSHOT in group
assert Permission.COMPUTER_CLICK not in group
def test_all_group_contains_everything(self):
"""Test that 'all' group contains all permissions."""
group = PERMISSION_GROUPS["all"]
assert len(group) == len(Permission)
class TestParsePermissions:
"""Tests for parse_permissions function."""
def test_empty_string_returns_empty_set(self):
"""Test that empty string returns empty set."""
result = parse_permissions("")
assert result == set()
def test_single_permission(self):
"""Test parsing a single permission."""
result = parse_permissions("sandbox:list")
assert result == {Permission.SANDBOX_LIST}
def test_multiple_permissions(self):
"""Test parsing multiple permissions."""
result = parse_permissions("sandbox:list,sandbox:create")
assert result == {Permission.SANDBOX_LIST, Permission.SANDBOX_CREATE}
def test_permission_with_spaces(self):
"""Test parsing permissions with spaces."""
result = parse_permissions("sandbox:list, sandbox:create")
assert result == {Permission.SANDBOX_LIST, Permission.SANDBOX_CREATE}
def test_group_expansion(self):
"""Test that groups are expanded."""
result = parse_permissions("sandbox:readonly")
assert Permission.SANDBOX_LIST in result
assert Permission.SANDBOX_GET in result
def test_all_group(self):
"""Test that 'all' group includes everything."""
result = parse_permissions("all")
assert len(result) == len(Permission)
def test_mixed_groups_and_permissions(self):
"""Test mixing groups and individual permissions."""
result = parse_permissions("sandbox:readonly,computer:screenshot")
assert Permission.SANDBOX_LIST in result
assert Permission.SANDBOX_GET in result
assert Permission.COMPUTER_SCREENSHOT in result
def test_unknown_permission_ignored(self):
"""Test that unknown permissions are ignored with warning."""
with patch("cua_cli.commands.mcp.logger") as mock_logger:
result = parse_permissions("sandbox:list,unknown:perm")
assert result == {Permission.SANDBOX_LIST}
mock_logger.warning.assert_called()
class TestRegisterParser:
"""Tests for register_parser function."""
def test_registers_serve_mcp_command(self):
"""Test that serve-mcp command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
register_parser(subparsers)
args = parser.parse_args(["serve-mcp"])
assert hasattr(args, "permissions")
def test_has_permissions_flag(self):
"""Test that --permissions flag is available."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
register_parser(subparsers)
args = parser.parse_args(["serve-mcp", "--permissions", "sandbox:all"])
assert args.permissions == "sandbox:all"
def test_has_sandbox_flag(self):
"""Test that --sandbox flag is available."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
register_parser(subparsers)
args = parser.parse_args(["serve-mcp", "--sandbox", "my-sandbox"])
assert args.sandbox == "my-sandbox"
class TestExecute:
"""Tests for execute function."""
def test_missing_fastmcp_returns_error(self, args_namespace, capsys):
"""Test that missing fastmcp dependency returns error."""
args = args_namespace(permissions="", sandbox="")
# Temporarily remove mcp modules to simulate ImportError
original_modules = {}
for mod in list(sys.modules.keys()):
if mod.startswith("mcp"):
original_modules[mod] = sys.modules.pop(mod)
try:
with patch.dict(
"sys.modules", {"mcp": None, "mcp.server": None, "mcp.server.fastmcp": None}
):
result = execute(args)
finally:
# Restore modules
sys.modules.update(original_modules)
assert result == 1
def test_permissions_from_env_var(self, args_namespace, monkeypatch):
"""Test that permissions are read from environment variable."""
monkeypatch.setenv("CUA_MCP_PERMISSIONS", "sandbox:readonly")
permissions = parse_permissions("sandbox:readonly")
assert Permission.SANDBOX_LIST in permissions
assert Permission.SANDBOX_CREATE not in permissions
def test_permissions_from_args_override_env(self, args_namespace, monkeypatch):
"""Test that command line args override environment variable."""
args = args_namespace(permissions="computer:all", sandbox="")
monkeypatch.setenv("CUA_MCP_PERMISSIONS", "sandbox:readonly")
# The args.permissions should take precedence
assert args.permissions == "computer:all"
class TestMCPToolRegistration:
"""Tests for MCP tool registration based on permissions."""
def test_permission_affects_tool_registration(self):
"""Test that permissions control which tools would be registered."""
# This is a unit test of the permission logic, not the actual registration
# The actual registration requires fastmcp which may not be installed
all_perms = parse_permissions("all")
assert len(all_perms) == len(Permission)
readonly_perms = parse_permissions("sandbox:readonly")
assert Permission.SANDBOX_LIST in readonly_perms
assert Permission.SANDBOX_CREATE not in readonly_perms
# Test that the registration functions exist and are callable
from cua_cli.commands.mcp import (
_register_computer_tools,
_register_sandbox_tools,
_register_skills_tools,
)
assert callable(_register_sandbox_tools)
assert callable(_register_computer_tools)
assert callable(_register_skills_tools)
@@ -0,0 +1,671 @@
"""Tests for sandbox command module."""
import argparse
from unittest.mock import AsyncMock, MagicMock, patch
from cua_cli.commands import sandbox
class TestRegisterParser:
"""Tests for register_parser function."""
def test_registers_sandbox_command(self):
"""Test that sandbox command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
sandbox.register_parser(subparsers)
# Parse a sandbox command to verify it's registered
args = parser.parse_args(["sandbox", "list"])
assert args.sandbox_command == "list"
def test_registers_sb_alias(self):
"""Test that sb alias is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
sandbox.register_parser(subparsers)
# Parse using alias
args = parser.parse_args(["sb", "list"])
assert args.sandbox_command == "list"
def test_list_command_has_json_flag(self):
"""Test that list command has --json flag."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
sandbox.register_parser(subparsers)
args = parser.parse_args(["sandbox", "list", "--json"])
assert args.json is True
def test_create_command_has_required_args(self):
"""Test that create command has required arguments."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
sandbox.register_parser(subparsers)
args = parser.parse_args(
[
"sandbox",
"create",
"--os",
"linux",
"--size",
"medium",
"--region",
"north-america",
]
)
assert args.os == "linux"
assert args.size == "medium"
assert args.region == "north-america"
class TestExecute:
"""Tests for execute function."""
def test_dispatch_to_list(self, args_namespace):
"""Test dispatch to list command."""
args = args_namespace(
command="sandbox", sandbox_command="list", json=False, show_passwords=False
)
with patch.object(sandbox, "cmd_list", return_value=0) as mock_cmd:
result = sandbox.execute(args)
mock_cmd.assert_called_once_with(args)
assert result == 0
def test_dispatch_to_list_alias(self, args_namespace):
"""Test dispatch to list command via ls alias."""
args = args_namespace(command="sb", sandbox_command="ls", json=False, show_passwords=False)
with patch.object(sandbox, "cmd_list", return_value=0) as mock_cmd:
sandbox.execute(args)
mock_cmd.assert_called_once_with(args)
def test_unknown_command_returns_error(self, args_namespace):
"""Test that unknown command returns error."""
args = args_namespace(command="sandbox", sandbox_command=None)
with patch.object(sandbox, "print_error") as mock_error:
result = sandbox.execute(args)
assert result == 1
mock_error.assert_called()
class TestCmdList:
"""Tests for cmd_list function."""
def test_list_sandboxes_success(self, args_namespace, sample_vm_list, mock_api_key):
"""Test listing sandboxes successfully."""
args = args_namespace(json=False, show_passwords=False)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.list_vms = AsyncMock(return_value=sample_vm_list)
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_table") as mock_print:
result = sandbox.cmd_list(args)
assert result == 0
mock_print.assert_called_once()
def test_list_sandboxes_empty(self, args_namespace, mock_api_key):
"""Test listing when no sandboxes exist."""
args = args_namespace(json=False, show_passwords=False)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.list_vms = AsyncMock(return_value=[])
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_info") as mock_print:
result = sandbox.cmd_list(args)
assert result == 0
mock_print.assert_called_with("No sandboxes found.")
def test_list_sandboxes_json_output(self, args_namespace, sample_vm_list, mock_api_key):
"""Test JSON output format."""
args = args_namespace(json=True, show_passwords=False)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.list_vms = AsyncMock(return_value=sample_vm_list)
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_json") as mock_print:
result = sandbox.cmd_list(args)
assert result == 0
mock_print.assert_called_once_with(sample_vm_list)
class TestCmdCreate:
"""Tests for cmd_create function."""
def test_create_sandbox_success(self, args_namespace, mock_api_key):
"""Test creating a sandbox successfully."""
args = args_namespace(
os="linux",
size="medium",
region="north-america",
json=False,
)
# Mock the API response (status 202 = provisioning)
async def mock_api_request(*args, **kwargs):
return (
202,
{
"status": "provisioning",
"name": "new-sandbox",
"host": "sandbox.example.com",
},
)
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
with patch.object(sandbox, "print_info"):
result = sandbox.cmd_create(args)
assert result == 0
def test_create_sandbox_error(self, args_namespace, mock_api_key):
"""Test handling create error."""
args = args_namespace(
os="linux",
size="medium",
region="north-america",
json=False,
)
# Mock the API response (status 500 = error)
async def mock_api_request(*args, **kwargs):
return (500, "Quota exceeded")
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
with patch.object(sandbox, "print_error") as mock_error:
result = sandbox.cmd_create(args)
assert result == 1
mock_error.assert_called()
class TestCmdGet:
"""Tests for cmd_get function."""
def test_get_sandbox_success(self, args_namespace, sample_vm, mock_api_key):
"""Test getting sandbox details."""
args = args_namespace(
name="test-sandbox-1",
json=False,
show_passwords=False,
show_vnc_url=False,
)
vm_dict = {
"name": sample_vm.name,
"status": sample_vm.status,
"os_type": sample_vm.os_type,
"host": "sandbox.example.com",
}
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.list_vms = AsyncMock(return_value=[vm_dict])
mock_provider.get_vm = AsyncMock(return_value={"status": "running"})
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_info"):
result = sandbox.cmd_get(args)
assert result == 0
def test_get_sandbox_not_found(self, args_namespace, mock_api_key):
"""Test getting nonexistent sandbox."""
args = args_namespace(
name="nonexistent",
json=False,
show_passwords=False,
show_vnc_url=False,
)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.list_vms = AsyncMock(return_value=[])
mock_provider.get_vm = AsyncMock(return_value={"status": "not_found"})
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_error") as mock_error:
result = sandbox.cmd_get(args)
assert result == 1
mock_error.assert_called()
class TestCmdStart:
"""Tests for cmd_start function."""
def test_start_sandbox_success(self, args_namespace, mock_api_key):
"""Test starting a sandbox."""
args = args_namespace(name="test-sandbox")
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.run_vm = AsyncMock(return_value={"status": "starting"})
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_success"):
result = sandbox.cmd_start(args)
assert result == 0
def test_start_sandbox_not_found(self, args_namespace, mock_api_key):
"""Test starting nonexistent sandbox."""
args = args_namespace(name="nonexistent")
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.run_vm = AsyncMock(return_value={"status": "not_found"})
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_error"):
result = sandbox.cmd_start(args)
assert result == 1
class TestCmdStop:
"""Tests for cmd_stop function."""
def test_stop_sandbox_success(self, args_namespace, mock_api_key):
"""Test stopping a sandbox."""
args = args_namespace(name="test-sandbox")
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.stop_vm = AsyncMock(return_value={"status": "stopping"})
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_success"):
result = sandbox.cmd_stop(args)
assert result == 0
class TestCmdRestart:
"""Tests for cmd_restart function."""
def test_restart_sandbox_success(self, args_namespace, mock_api_key):
"""Test restarting a sandbox."""
args = args_namespace(name="test-sandbox")
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.restart_vm = AsyncMock(return_value={"status": "restarting"})
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_success"):
result = sandbox.cmd_restart(args)
assert result == 0
class TestCmdSuspend:
"""Tests for cmd_suspend function."""
def test_suspend_sandbox_success(self, args_namespace, mock_api_key):
"""Test suspending a sandbox."""
args = args_namespace(name="test-sandbox")
# Mock the API response (status 202 = suspending)
async def mock_api_request(*args, **kwargs):
return (202, {"status": "suspending"})
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
with patch.object(sandbox, "print_success"):
result = sandbox.cmd_suspend(args)
assert result == 0
def test_suspend_unsupported(self, args_namespace, mock_api_key):
"""Test suspend on unsupported sandbox."""
args = args_namespace(name="test-sandbox")
# Mock the API response (status 400 = unsupported)
async def mock_api_request(*args, **kwargs):
return (400, "Suspend not supported for Windows")
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
with patch.object(sandbox, "print_error"):
result = sandbox.cmd_suspend(args)
assert result == 1
class TestCmdDelete:
"""Tests for cmd_delete function."""
def test_delete_sandbox_success(self, args_namespace, mock_api_key):
"""Test deleting a sandbox."""
args = args_namespace(name="test-sandbox")
# Mock the API response (status 202 = deleting)
async def mock_api_request(*args, **kwargs):
return (202, {"status": "deleting"})
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
with patch.object(sandbox, "print_success"):
result = sandbox.cmd_delete(args)
assert result == 0
class TestCmdVnc:
"""Tests for cmd_vnc function."""
def test_vnc_opens_browser(self, args_namespace, mock_api_key, mock_webbrowser):
"""Test VNC opens browser with correct URL."""
args = args_namespace(name="test-sandbox")
vm_info = {
"name": "test-sandbox",
"vnc_url": "https://vnc.example.com/test",
}
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.list_vms = AsyncMock(return_value=[vm_info])
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_info"):
result = sandbox.cmd_vnc(args)
assert result == 0
mock_webbrowser.assert_called_once_with("https://vnc.example.com/test")
def test_vnc_constructs_url_from_host(self, args_namespace, mock_api_key, mock_webbrowser):
"""Test VNC constructs URL when vnc_url not provided."""
args = args_namespace(name="test-sandbox")
vm_info = {
"name": "test-sandbox",
"host": "sandbox.example.com",
"password": "secret123",
}
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.list_vms = AsyncMock(return_value=[vm_info])
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_info"):
result = sandbox.cmd_vnc(args)
assert result == 0
mock_webbrowser.assert_called_once()
# Check URL contains host and encoded password
call_url = mock_webbrowser.call_args[0][0]
assert "sandbox.example.com" in call_url
assert "secret123" in call_url
def test_vnc_sandbox_not_found(self, args_namespace, mock_api_key):
"""Test VNC with nonexistent sandbox."""
args = args_namespace(name="nonexistent")
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.list_vms = AsyncMock(return_value=[])
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_error"):
result = sandbox.cmd_vnc(args)
assert result == 1
class TestCmdShell:
"""Tests for cmd_shell function."""
def test_shell_parser_registration(self):
"""Test that shell command is registered with correct arguments."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
sandbox.register_parser(subparsers)
# Test basic shell command
args = parser.parse_args(["sb", "shell", "my-sandbox"])
assert args.name == "my-sandbox"
assert args.sandbox_command == "shell"
def test_shell_with_command(self):
"""Test shell command with a command to execute."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
sandbox.register_parser(subparsers)
args = parser.parse_args(["sb", "shell", "my-sandbox", "ls", "-la"])
assert args.name == "my-sandbox"
assert args.shell_command == ["ls", "-la"]
def test_shell_with_cols_rows(self):
"""Test shell command with terminal size options.
Options must come before name due to argparse REMAINDER behavior.
Usage: cua sb shell --cols 120 --rows 40 mybox [command...]
"""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
sandbox.register_parser(subparsers)
args = parser.parse_args(
["sb", "shell", "--cols", "120", "--rows", "40", "my-sandbox", "ls"]
)
assert args.cols == 120
assert args.rows == 40
assert args.name == "my-sandbox"
assert args.shell_command == ["ls"]
def test_shell_sandbox_not_found(self, args_namespace, mock_api_key):
"""Test shell with nonexistent sandbox."""
args = args_namespace(
name="nonexistent",
shell_command=[],
cols=None,
rows=None,
)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.get_vm = AsyncMock(return_value={"status": "not_found"})
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_error") as mock_error:
with patch("sys.stdin") as mock_stdin:
mock_stdin.isatty.return_value = False
result = sandbox.cmd_shell(args)
assert result == 1
mock_error.assert_called()
def test_shell_no_api_url(self, args_namespace, mock_api_key):
"""Test shell when sandbox has no API URL."""
args = args_namespace(
name="test-sandbox",
shell_command=["echo", "hello"],
cols=None,
rows=None,
)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.get_vm = AsyncMock(return_value={"status": "stopped", "api_url": None})
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_error") as mock_error:
with patch("sys.stdin") as mock_stdin:
mock_stdin.isatty.return_value = False
result = sandbox.cmd_shell(args)
assert result == 1
mock_error.assert_called()
class TestCmdExec:
"""Tests for cmd_exec function."""
def test_exec_parser_registration(self):
"""Test that exec command is registered with correct arguments."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
sandbox.register_parser(subparsers)
args = parser.parse_args(["sb", "exec", "my-sandbox", "echo", "hello"])
assert args.name == "my-sandbox"
assert args.sandbox_command == "exec"
assert args.exec_command == ["echo", "hello"]
def test_exec_with_json_flag(self):
"""Test exec command with --json flag.
--json must come before name due to argparse REMAINDER behavior.
Usage: cua sb exec --json mybox <command...>
"""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
sandbox.register_parser(subparsers)
args = parser.parse_args(["sb", "exec", "--json", "my-sandbox", "echo", "hello"])
assert args.json is True
assert args.name == "my-sandbox"
assert args.exec_command == ["echo", "hello"]
def test_exec_no_command_error(self, args_namespace, mock_api_key):
"""Test exec with no command returns error."""
args = args_namespace(
name="test-sandbox",
exec_command=[],
json=False,
)
with patch.object(sandbox, "print_error") as mock_error:
result = sandbox.cmd_exec(args)
assert result == 1
mock_error.assert_called_with("No command provided")
def test_exec_sandbox_not_found(self, args_namespace, mock_api_key):
"""Test exec with nonexistent sandbox."""
args = args_namespace(
name="nonexistent",
exec_command=["echo", "hello"],
json=False,
)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.get_vm = AsyncMock(return_value={"status": "not_found"})
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "print_error") as mock_error:
result = sandbox.cmd_exec(args)
assert result == 1
mock_error.assert_called()
def test_exec_success(self, args_namespace, mock_api_key, capsys):
"""Test successful command execution."""
args = args_namespace(
name="test-sandbox",
exec_command=["echo", "hello"],
json=False,
)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.get_vm = AsyncMock(
return_value={"status": "running", "api_url": "https://sandbox.example.com:8443"}
)
async def mock_exec(*a, **kw):
return {"success": True, "stdout": "hello\n", "stderr": "", "returncode": 0}
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "_exec_noninteractive", side_effect=mock_exec):
result = sandbox.cmd_exec(args)
assert result == 0
captured = capsys.readouterr()
assert "hello" in captured.out
def test_exec_json_output(self, args_namespace, mock_api_key):
"""Test exec with JSON output."""
args = args_namespace(
name="test-sandbox",
exec_command=["echo", "hello"],
json=True,
)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.get_vm = AsyncMock(
return_value={"status": "running", "api_url": "https://sandbox.example.com:8443"}
)
async def mock_exec(*a, **kw):
return {"success": True, "stdout": "hello\n", "stderr": "", "returncode": 0}
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "_exec_noninteractive", side_effect=mock_exec):
with patch.object(sandbox, "print_json") as mock_json:
result = sandbox.cmd_exec(args)
assert result == 0
mock_json.assert_called_once()
def test_exec_command_failure(self, args_namespace, mock_api_key, capsys):
"""Test exec when command returns non-zero exit code."""
args = args_namespace(
name="test-sandbox",
exec_command=["false"],
json=False,
)
mock_provider = MagicMock()
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
mock_provider.__aexit__ = AsyncMock(return_value=None)
mock_provider.get_vm = AsyncMock(
return_value={"status": "running", "api_url": "https://sandbox.example.com:8443"}
)
async def mock_exec(*a, **kw):
return {"success": True, "stdout": "", "stderr": "error", "returncode": 1}
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
with patch.object(sandbox, "_exec_noninteractive", side_effect=mock_exec):
result = sandbox.cmd_exec(args)
assert result == 1
@@ -0,0 +1,235 @@
"""Tests for skills command module."""
import argparse
from unittest.mock import patch
from cua_cli.commands import skills
class TestRegisterParser:
"""Tests for register_parser function."""
def test_registers_skills_command(self):
"""Test that skills command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
skills.register_parser(subparsers)
args = parser.parse_args(["skills", "list"])
assert args.skills_command == "list"
def test_record_command_exists(self):
"""Test that record command is registered."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
skills.register_parser(subparsers)
args = parser.parse_args(["skills", "record"])
assert args.skills_command == "record"
def test_record_command_has_sandbox_flag(self):
"""Test that record command has --sandbox flag."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
skills.register_parser(subparsers)
args = parser.parse_args(["skills", "record", "--sandbox", "my-sandbox"])
assert args.sandbox == "my-sandbox"
def test_read_command_has_name_arg(self):
"""Test that read command has name argument."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
skills.register_parser(subparsers)
args = parser.parse_args(["skills", "read", "my-skill"])
assert args.name == "my-skill"
class TestExecute:
"""Tests for execute function."""
def test_dispatch_to_list(self, args_namespace):
"""Test dispatch to list command."""
args = args_namespace(command="skills", skills_command="list", json=False)
with patch.object(skills, "cmd_list", return_value=0) as mock_cmd:
result = skills.execute(args)
mock_cmd.assert_called_once_with(args)
assert result == 0
def test_dispatch_to_read(self, args_namespace):
"""Test dispatch to read command."""
args = args_namespace(command="skills", skills_command="read", name="my-skill")
with patch.object(skills, "cmd_read", return_value=0) as mock_cmd:
skills.execute(args)
mock_cmd.assert_called_once_with(args)
def test_dispatch_to_delete(self, args_namespace):
"""Test dispatch to delete command."""
args = args_namespace(command="skills", skills_command="delete", name="my-skill")
with patch.object(skills, "cmd_delete", return_value=0) as mock_cmd:
skills.execute(args)
mock_cmd.assert_called_once_with(args)
def test_unknown_command_returns_error(self, args_namespace):
"""Test that unknown command returns error."""
args = args_namespace(command="skills", skills_command=None)
with patch.object(skills, "print_error"):
result = skills.execute(args)
assert result == 1
class TestCmdList:
"""Tests for cmd_list function."""
def test_list_empty_directory(self, args_namespace, temp_skills_dir):
"""Test listing when no skills exist."""
args = args_namespace(json=False)
with patch.object(skills, "print_info") as mock_print:
result = skills.cmd_list(args)
assert result == 0
mock_print.assert_called()
def test_list_with_skills(self, args_namespace, sample_skill, temp_skills_dir):
"""Test listing existing skills."""
args = args_namespace(json=False)
with patch.object(skills, "print_table") as mock_print:
result = skills.cmd_list(args)
assert result == 0
mock_print.assert_called_once()
# Verify the skill is in the output
call_args = mock_print.call_args[0]
skill_data = call_args[0]
assert len(skill_data) == 1
assert skill_data[0]["name"] == "test-skill"
def test_list_json_output(self, args_namespace, sample_skill, temp_skills_dir):
"""Test JSON output format."""
args = args_namespace(json=True)
with patch.object(skills, "print_json") as mock_print:
result = skills.cmd_list(args)
assert result == 0
mock_print.assert_called_once()
class TestCmdRead:
"""Tests for cmd_read function."""
def test_read_existing_skill(self, args_namespace, sample_skill, temp_skills_dir, capsys):
"""Test reading an existing skill."""
args = args_namespace(name="test-skill", format="md")
result = skills.cmd_read(args)
assert result == 0
# Verify content was printed to stdout
captured = capsys.readouterr()
assert "test-skill" in captured.out
def test_read_nonexistent_skill(self, args_namespace, temp_skills_dir):
"""Test reading a nonexistent skill."""
args = args_namespace(name="nonexistent", format="md")
with patch.object(skills, "print_error") as mock_error:
result = skills.cmd_read(args)
assert result == 1
mock_error.assert_called()
class TestCmdDelete:
"""Tests for cmd_delete function."""
def test_delete_existing_skill(self, args_namespace, sample_skill, temp_skills_dir):
"""Test deleting an existing skill."""
args = args_namespace(name="test-skill")
assert sample_skill.exists()
with patch.object(skills, "print_success"):
result = skills.cmd_delete(args)
assert result == 0
assert not sample_skill.exists()
def test_delete_nonexistent_skill(self, args_namespace, temp_skills_dir):
"""Test deleting a nonexistent skill."""
args = args_namespace(name="nonexistent")
with patch.object(skills, "print_error") as mock_error:
result = skills.cmd_delete(args)
assert result == 1
mock_error.assert_called()
class TestCmdClean:
"""Tests for cmd_clean function."""
def test_clean_removes_all_skills(self, args_namespace, sample_skill, temp_skills_dir):
"""Test that clean removes all skills."""
args = args_namespace()
assert sample_skill.exists()
with patch("builtins.input", return_value="y"):
with patch.object(skills, "print_success"):
result = skills.cmd_clean(args)
assert result == 0
# Verify no skills remain
remaining = list(temp_skills_dir.iterdir())
assert len(remaining) == 0
def test_clean_empty_directory(self, args_namespace, temp_skills_dir):
"""Test clean on empty directory."""
args = args_namespace()
with patch.object(skills, "print_info"):
result = skills.cmd_clean(args)
assert result == 0
class TestCmdReplay:
"""Tests for cmd_replay function."""
def test_replay_existing_skill(
self, args_namespace, sample_skill, temp_skills_dir, mock_webbrowser
):
"""Test replaying an existing skill opens the video."""
args = args_namespace(name="test-skill")
with patch.object(skills, "print_info"):
result = skills.cmd_replay(args)
assert result == 0
# Verify webbrowser was called with the video path
mock_webbrowser.assert_called_once()
call_url = mock_webbrowser.call_args[0][0]
assert "test-skill.mp4" in call_url
def test_replay_nonexistent_skill(self, args_namespace, temp_skills_dir):
"""Test replaying a nonexistent skill."""
args = args_namespace(name="nonexistent")
with patch.object(skills, "print_error") as mock_error:
result = skills.cmd_replay(args)
assert result == 1
mock_error.assert_called()
+254
View File
@@ -0,0 +1,254 @@
"""Shared test fixtures for cua-cli tests."""
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# Mock external modules before importing cua_cli
# This allows tests to run without cua-computer installed
mock_providers = MagicMock()
mock_providers.VMProviderFactory = MagicMock()
mock_providers.VMProviderType = MagicMock()
mock_providers.VMProviderType.CLOUD = "cloud"
mock_computer = MagicMock()
mock_computer.providers = mock_providers
mock_computer.providers.cloud = MagicMock()
mock_computer.providers.cloud.provider = MagicMock()
mock_computer.providers.cloud.provider.CloudProvider = MagicMock()
sys.modules["computer"] = mock_computer
sys.modules["computer.providers"] = mock_providers
sys.modules["computer.providers.cloud"] = mock_computer.providers.cloud
sys.modules["computer.providers.cloud.provider"] = mock_computer.providers.cloud.provider
@pytest.fixture
def disable_telemetry(monkeypatch):
"""Disable telemetry for tests."""
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", "false")
@pytest.fixture
def temp_credentials_db(tmp_path, monkeypatch):
"""Provide a temporary credentials database."""
cua_dir = tmp_path / ".cua"
cua_dir.mkdir(parents=True)
db_path = cua_dir / "credentials.db"
# Patch the CREDENTIALS_DB path in the store module
monkeypatch.setattr("cua_cli.auth.store.CREDENTIALS_DB", db_path)
yield db_path
if db_path.exists():
db_path.unlink()
@pytest.fixture
def mock_api_key(monkeypatch):
"""Set a mock API key in environment."""
monkeypatch.setenv("CUA_API_KEY", "test-api-key-12345")
return "test-api-key-12345"
@pytest.fixture
def clear_api_key_env(monkeypatch):
"""Ensure no API key is in environment."""
monkeypatch.delenv("CUA_API_KEY", raising=False)
@pytest.fixture
def mock_aiohttp_session():
"""Mock aiohttp.ClientSession for API tests."""
with patch("aiohttp.ClientSession") as mock_session:
mock_instance = AsyncMock()
mock_session.return_value.__aenter__.return_value = mock_instance
mock_session.return_value.__aexit__.return_value = None
yield mock_instance
@pytest.fixture
def mock_cloud_provider():
"""Mock CloudProvider for sandbox tests."""
with patch("cua_cli.commands.sandbox.VMProviderFactory") as mock_factory:
mock_provider = AsyncMock()
mock_provider.__aenter__.return_value = mock_provider
mock_provider.__aexit__.return_value = None
mock_factory.create_provider.return_value = mock_provider
yield mock_provider
@pytest.fixture
def sample_vm():
"""Sample VM object for testing."""
vm = MagicMock()
vm.name = "test-sandbox-1"
vm.status = "running"
vm.os_type = "linux"
vm.created_at = "2024-01-15T10:00:00Z"
vm.size = "medium"
vm.region = "north-america"
vm.vnc_url = "https://vnc.example.com/test-sandbox-1"
vm.server_url = "https://server.example.com:8000"
return vm
@pytest.fixture
def sample_vm_list(sample_vm):
"""Sample VM list for testing."""
vm2 = MagicMock()
vm2.name = "test-sandbox-2"
vm2.status = "stopped"
vm2.os_type = "macos"
vm2.created_at = "2024-01-14T08:30:00Z"
vm2.size = "large"
vm2.region = "europe"
vm2.vnc_url = None
vm2.server_url = None
return [sample_vm, vm2]
@pytest.fixture
def temp_skills_dir(tmp_path, monkeypatch):
"""Provide a temporary skills directory."""
skills_dir = tmp_path / ".cua" / "skills"
skills_dir.mkdir(parents=True)
# Patch the SKILLS_DIR in the skills module
monkeypatch.setattr("cua_cli.commands.skills.SKILLS_DIR", skills_dir)
return skills_dir
@pytest.fixture
def sample_skill(temp_skills_dir):
"""Create a sample skill for testing."""
skill_dir = temp_skills_dir / "test-skill"
skill_dir.mkdir()
# Create SKILL.md with proper frontmatter format expected by _parse_frontmatter
skill_file = skill_dir / "SKILL.md"
skill_file.write_text("""---
name: test-skill
description: A sample skill for testing
---
# Test Skill
A sample skill for testing.
## Steps
1. Click on the button
2. Type some text
3. Press Enter
""")
# Create trajectory directory with trajectory.json and a video file
trajectory_dir = skill_dir / "trajectory"
trajectory_dir.mkdir()
# Create trajectory.json for proper skill info extraction
trajectory_json = trajectory_dir / "trajectory.json"
trajectory_json.write_text("""{
"trajectory": [
{"step_idx": 1, "caption": {"action": "click"}},
{"step_idx": 2, "caption": {"action": "type"}}
],
"metadata": {
"created_at": "2024-01-15T10:00:00Z"
}
}""")
# Create a fake MP4 file for replay tests
video_file = trajectory_dir / "test-skill.mp4"
video_file.write_bytes(b"fake mp4 content")
return skill_dir
@pytest.fixture
def mock_image_api_client():
"""Mock CloudAPIClient for image tests."""
with patch("cua_cli.commands.image.CloudAPIClient") as mock_client_class:
mock_client = AsyncMock()
mock_client_class.return_value = mock_client
yield mock_client
@pytest.fixture
def sample_cloud_images():
"""Sample cloud images for testing."""
return [
{
"name": "ubuntu-22.04",
"image_type": "qcow2",
"created_at": "2024-01-10T12:00:00Z",
"versions": [
{
"tag": "latest",
"size_bytes": 5368709120,
"status": "ready",
"created_at": "2024-01-10T12:00:00Z",
},
{
"tag": "v1.0",
"size_bytes": 5000000000,
"status": "ready",
"created_at": "2024-01-05T10:00:00Z",
},
],
},
{
"name": "macos-sonoma",
"image_type": "qcow2",
"created_at": "2024-01-08T09:00:00Z",
"versions": [
{
"tag": "latest",
"size_bytes": 21474836480,
"status": "ready",
"created_at": "2024-01-08T09:00:00Z",
},
],
},
]
@pytest.fixture
def temp_image_file(tmp_path):
"""Create a temporary image file for upload tests."""
image_file = tmp_path / "test-image" / "data.img"
image_file.parent.mkdir(parents=True)
# Create a small test file
image_file.write_bytes(b"fake image content" * 1000)
return image_file
@pytest.fixture
def mock_webbrowser():
"""Mock webbrowser.open for browser tests."""
with patch("webbrowser.open") as mock_open:
yield mock_open
@pytest.fixture
def mock_rich_console():
"""Mock rich console for output tests."""
with patch("cua_cli.utils.output.Console") as mock_console_class:
mock_console = MagicMock()
mock_console_class.return_value = mock_console
yield mock_console
@pytest.fixture
def args_namespace():
"""Create an argparse.Namespace factory for command tests."""
import argparse
def create_args(**kwargs):
return argparse.Namespace(**kwargs)
return create_args
+166
View File
@@ -0,0 +1,166 @@
"""Tests for the main CLI entry point."""
import argparse
import sys
from unittest.mock import patch
import pytest
from cua_cli.main import create_parser, main
class TestCreateParser:
"""Tests for create_parser function."""
def test_creates_parser(self):
"""Test that parser is created."""
parser = create_parser()
assert isinstance(parser, argparse.ArgumentParser)
assert parser.prog == "cua"
def test_has_version_flag(self):
"""Test that --version flag is present."""
parser = create_parser()
with pytest.raises(SystemExit) as exc_info:
parser.parse_args(["--version"])
assert exc_info.value.code == 0
def test_has_auth_command(self):
"""Test that auth command is registered."""
parser = create_parser()
args = parser.parse_args(["auth", "login"])
assert args.command == "auth"
assert args.auth_command == "login"
def test_has_sandbox_command(self):
"""Test that sandbox command is registered."""
parser = create_parser()
args = parser.parse_args(["sandbox", "list"])
assert args.command == "sandbox"
assert args.sandbox_command == "list"
def test_has_sb_alias(self):
"""Test that sb alias works."""
parser = create_parser()
args = parser.parse_args(["sb", "list"])
assert args.command == "sb"
assert args.sandbox_command == "list"
def test_has_image_command(self):
"""Test that image command is registered."""
parser = create_parser()
args = parser.parse_args(["image", "list"])
assert args.command == "image"
assert args.image_command == "list"
def test_has_skills_command(self):
"""Test that skills command is registered."""
parser = create_parser()
args = parser.parse_args(["skills", "list"])
assert args.command == "skills"
assert args.skills_command == "list"
def test_has_serve_mcp_command(self):
"""Test that serve-mcp command is registered."""
parser = create_parser()
args = parser.parse_args(["serve-mcp"])
assert args.command == "serve-mcp"
class TestMain:
"""Tests for main function."""
def test_no_args_shows_help(self, capsys):
"""Test that no arguments shows help."""
with patch.object(sys, "argv", ["cua"]):
result = main()
assert result == 0
captured = capsys.readouterr()
assert "usage:" in captured.out.lower() or "cua" in captured.out
def test_dispatch_to_auth(self):
"""Test dispatch to auth command."""
with patch.object(sys, "argv", ["cua", "auth", "logout"]):
with patch("cua_cli.commands.auth.execute", return_value=0) as mock_execute:
result = main()
mock_execute.assert_called_once()
assert result == 0
def test_dispatch_to_sandbox(self):
"""Test dispatch to sandbox command."""
with patch.object(sys, "argv", ["cua", "sandbox", "list"]):
with patch("cua_cli.commands.sandbox.execute", return_value=0) as mock_execute:
result = main()
mock_execute.assert_called_once()
assert result == 0
def test_dispatch_to_sb_alias(self):
"""Test dispatch to sb alias."""
with patch.object(sys, "argv", ["cua", "sb", "list"]):
with patch("cua_cli.commands.sandbox.execute", return_value=0) as mock_execute:
result = main()
mock_execute.assert_called_once()
assert result == 0
def test_dispatch_to_image(self):
"""Test dispatch to image command."""
with patch.object(sys, "argv", ["cua", "image", "list"]):
with patch("cua_cli.commands.image.execute", return_value=0) as mock_execute:
result = main()
mock_execute.assert_called_once()
assert result == 0
def test_dispatch_to_skills(self):
"""Test dispatch to skills command."""
with patch.object(sys, "argv", ["cua", "skills", "list"]):
with patch("cua_cli.commands.skills.execute", return_value=0) as mock_execute:
result = main()
mock_execute.assert_called_once()
assert result == 0
def test_dispatch_to_mcp(self):
"""Test dispatch to serve-mcp command."""
with patch.object(sys, "argv", ["cua", "serve-mcp"]):
with patch("cua_cli.commands.mcp.execute", return_value=0) as mock_execute:
result = main()
mock_execute.assert_called_once()
assert result == 0
def test_keyboard_interrupt_returns_130(self):
"""Test that KeyboardInterrupt returns exit code 130."""
with patch.object(sys, "argv", ["cua", "sandbox", "list"]):
with patch("cua_cli.commands.sandbox.execute", side_effect=KeyboardInterrupt):
result = main()
assert result == 130
def test_exception_returns_1(self):
"""Test that exceptions return exit code 1."""
with patch.object(sys, "argv", ["cua", "sandbox", "list"]):
with patch("cua_cli.commands.sandbox.execute", side_effect=Exception("Test error")):
result = main()
assert result == 1
def test_unknown_command_returns_1(self):
"""Test that unknown commands are handled."""
# Mock parse_args to return an unknown command
with patch.object(sys, "argv", ["cua", "unknown"]):
# This should trigger SystemExit from argparse
with pytest.raises(SystemExit):
main()
@@ -0,0 +1 @@
"""Utils module tests."""
@@ -0,0 +1,95 @@
"""Tests for async utilities."""
import asyncio
import pytest
from cua_cli.utils.async_utils import run_async
class TestRunAsync:
"""Tests for run_async function."""
def test_runs_coroutine(self):
"""Test that coroutine is executed."""
async def sample_coro():
return "result"
result = run_async(sample_coro())
assert result == "result"
def test_returns_correct_value(self):
"""Test that return value is correct."""
async def add(a, b):
return a + b
result = run_async(add(2, 3))
assert result == 5
def test_propagates_exception(self):
"""Test that exceptions are propagated."""
async def failing_coro():
raise ValueError("Test error")
with pytest.raises(ValueError) as exc_info:
run_async(failing_coro())
assert "Test error" in str(exc_info.value)
def test_handles_async_sleep(self):
"""Test that async sleep works correctly."""
async def with_sleep():
await asyncio.sleep(0.01)
return "done"
result = run_async(with_sleep())
assert result == "done"
def test_handles_nested_coroutines(self):
"""Test that nested coroutines work."""
async def inner():
return 42
async def outer():
return await inner()
result = run_async(outer())
assert result == 42
def test_handles_none_result(self):
"""Test that None result is handled."""
async def returns_none():
pass
result = run_async(returns_none())
assert result is None
def test_handles_list_result(self):
"""Test that list result is handled."""
async def returns_list():
return [1, 2, 3]
result = run_async(returns_list())
assert result == [1, 2, 3]
def test_handles_dict_result(self):
"""Test that dict result is handled."""
async def returns_dict():
return {"key": "value"}
result = run_async(returns_dict())
assert result == {"key": "value"}
@@ -0,0 +1,163 @@
"""Tests for output utilities."""
from unittest.mock import patch
from cua_cli.utils.output import (
print_error,
print_info,
print_json,
print_success,
print_table,
print_warning,
)
class TestPrintTable:
"""Tests for print_table function."""
def test_prints_table_with_data(self):
"""Test printing a table with data."""
data = [
{"name": "test1", "status": "running"},
{"name": "test2", "status": "stopped"},
]
columns = [("name", "NAME"), ("status", "STATUS")]
with patch("cua_cli.utils.output.console") as mock_console:
print_table(data, columns)
mock_console.print.assert_called_once()
def test_handles_empty_data(self):
"""Test handling empty data list."""
data = []
with patch("cua_cli.utils.output.console") as mock_console:
print_table(data)
mock_console.print.assert_called_once()
# Should print "No data" message
call_args = mock_console.print.call_args[0][0]
assert "No data" in call_args
def test_auto_generates_columns(self):
"""Test that columns are auto-generated from data keys."""
data = [{"foo": "bar", "baz": "qux"}]
with patch("cua_cli.utils.output.console") as mock_console:
print_table(data)
mock_console.print.assert_called_once()
def test_handles_missing_keys(self):
"""Test handling data items with missing keys."""
data = [
{"name": "test1", "status": "running"},
{"name": "test2"}, # Missing status
]
columns = [("name", "NAME"), ("status", "STATUS")]
with patch("cua_cli.utils.output.console") as mock_console:
print_table(data, columns)
mock_console.print.assert_called_once()
def test_with_title(self):
"""Test table with title."""
data = [{"name": "test"}]
with patch("cua_cli.utils.output.console") as mock_console:
print_table(data, title="Test Table")
mock_console.print.assert_called_once()
class TestPrintJson:
"""Tests for print_json function."""
def test_prints_dict(self):
"""Test printing a dictionary as JSON."""
data = {"key": "value", "number": 42}
with patch("cua_cli.utils.output.console") as mock_console:
print_json(data)
mock_console.print_json.assert_called_once()
def test_prints_list(self):
"""Test printing a list as JSON."""
data = [{"name": "test1"}, {"name": "test2"}]
with patch("cua_cli.utils.output.console") as mock_console:
print_json(data)
mock_console.print_json.assert_called_once()
def test_handles_nested_data(self):
"""Test printing nested data structures."""
data = {
"level1": {
"level2": {
"value": 123,
}
}
}
with patch("cua_cli.utils.output.console") as mock_console:
print_json(data)
mock_console.print_json.assert_called_once()
class TestPrintError:
"""Tests for print_error function."""
def test_prints_to_stderr(self):
"""Test that error is printed to stderr."""
with patch("cua_cli.utils.output.error_console") as mock_console:
print_error("Test error message")
mock_console.print.assert_called_once()
call_args = mock_console.print.call_args[0][0]
assert "Error:" in call_args
assert "Test error message" in call_args
class TestPrintSuccess:
"""Tests for print_success function."""
def test_prints_success_message(self):
"""Test printing success message."""
with patch("cua_cli.utils.output.console") as mock_console:
print_success("Operation successful")
mock_console.print.assert_called_once()
call_args = mock_console.print.call_args[0][0]
assert "Operation successful" in call_args
class TestPrintWarning:
"""Tests for print_warning function."""
def test_prints_warning_message(self):
"""Test printing warning message."""
with patch("cua_cli.utils.output.console") as mock_console:
print_warning("This is a warning")
mock_console.print.assert_called_once()
call_args = mock_console.print.call_args[0][0]
assert "Warning:" in call_args
assert "This is a warning" in call_args
class TestPrintInfo:
"""Tests for print_info function."""
def test_prints_info_message(self):
"""Test printing info message."""
with patch("cua_cli.utils.output.console") as mock_console:
print_info("Information message")
mock_console.print.assert_called_once()
call_args = mock_console.print.call_args[0][0]
assert "Information message" in call_args