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 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user