Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

276 lines
9.8 KiB
Python

"""Tests for platform.deployment.gateway.direct_deploy."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
import platform.deployment.gateway.direct_deploy as direct_module
from platform.deployment.aws.config import EC2_UBUNTU_ROOT_DEVICE_NAME
_INSTANCE_ID = "i-direct123"
_PUBLIC_IP = "1.2.3.4"
_BASE_AMI = "ami-base123"
_FAKE_PROFILE = {
"ProfileName": "opensre-gateway-direct-profile",
"ProfileArn": "arn:aws:iam::1:instance-profile/p",
"RoleName": "opensre-gateway-direct-role",
}
def _stub_deploy(monkeypatch: pytest.MonkeyPatch) -> dict[str, list[str]]:
"""Stub every external call in deploy_direct() and return captured SSM calls."""
calls: dict[str, list[str]] = {"ssm_commands": [], "launch_kwargs": []}
monkeypatch.setattr(direct_module, "create_instance_profile", lambda *_a, **_kw: _FAKE_PROFILE)
monkeypatch.setattr(direct_module, "get_latest_ubuntu2204_ami", lambda _region: _BASE_AMI)
monkeypatch.setattr(
direct_module, "create_stack_security_group", lambda *_a, **_kw: "sg-direct123"
)
def fake_launch_instance(*_args: object, **kwargs: object) -> dict[str, str]:
calls["launch_kwargs"].append(kwargs)
return {"InstanceId": _INSTANCE_ID}
monkeypatch.setattr(direct_module, "launch_instance", fake_launch_instance)
monkeypatch.setattr(
direct_module,
"wait_for_running",
lambda *_a, **_kw: {"PublicIpAddress": _PUBLIC_IP},
)
monkeypatch.setattr(direct_module, "wait_for_ssm_registration", lambda *_a, **_kw: None)
def fake_run_ssm(instance_id: str, *, commands: list[str], **_kw: object) -> dict[str, str]:
calls["ssm_commands"].extend(commands)
return {"status": "Success", "stdout": "", "stderr": ""}
monkeypatch.setattr(direct_module, "run_ssm_shell_command", fake_run_ssm)
monkeypatch.setattr(direct_module, "provision_gateway_via_ssm", lambda *_a, **_kw: None)
monkeypatch.setattr(direct_module, "wait_for_gateway_ready", lambda *_a, **_kw: None)
monkeypatch.setattr(direct_module, "_cleanup_existing", lambda **_kw: False)
return calls
def test_deploy_direct_returns_all_required_keys(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""deploy_direct() must return all expected output keys."""
monkeypatch.setattr(direct_module, "_OUTPUTS_DIR", tmp_path)
_stub_deploy(monkeypatch)
outputs = direct_module.deploy_direct(env_vars={}, region="us-east-1")
assert outputs["InstanceId"] == _INSTANCE_ID
assert outputs["PublicIpAddress"] == _PUBLIC_IP
assert outputs["BaseAmiId"] == _BASE_AMI
assert outputs["SecurityGroupId"] == "sg-direct123"
assert "ProfileName" in outputs
assert "RoleName" in outputs
# GitRef is no longer in outputs (no git clone with curl installer)
assert "GitRef" not in outputs
def test_deploy_direct_persists_outputs(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""deploy_direct() must write outputs to the expected path."""
monkeypatch.setattr(direct_module, "_OUTPUTS_DIR", tmp_path)
_stub_deploy(monkeypatch)
direct_module.deploy_direct(env_vars={}, region="us-east-1")
stack_name = direct_module._direct_stack_name()
output_file = tmp_path / f"{stack_name}.json"
assert output_file.exists()
data = json.loads(output_file.read_text())
assert data["InstanceId"] == _INSTANCE_ID
def test_deploy_direct_uses_ubuntu_root_device_for_volume_resize(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""Ubuntu AMIs use /dev/sda1; direct deploy must pass that root device name."""
monkeypatch.setattr(direct_module, "_OUTPUTS_DIR", tmp_path)
calls = _stub_deploy(monkeypatch)
direct_module.deploy_direct(env_vars={}, region="us-east-1")
assert calls["launch_kwargs"]
assert calls["launch_kwargs"][0]["root_device_name"] == EC2_UBUNTU_ROOT_DEVICE_NAME
def test_deploy_direct_uses_curl_installer(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""Install commands must use the curl installer URL, not pip or git."""
monkeypatch.setattr(direct_module, "_OUTPUTS_DIR", tmp_path)
calls = _stub_deploy(monkeypatch)
direct_module.deploy_direct(env_vars={}, region="us-east-1")
joined = "\n".join(calls["ssm_commands"])
assert direct_module._INSTALL_URL in joined
assert "curl" in joined
assert "pip install" not in joined
assert "git+" not in joined
def test_deploy_direct_binary_path_in_service_unit(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""The embedded systemd unit must use /usr/local/bin/opensre, not the venv path."""
monkeypatch.setattr(direct_module, "_OUTPUTS_DIR", tmp_path)
_stub_deploy(monkeypatch)
direct_module.deploy_direct(env_vars={}, region="us-east-1")
assert "/usr/local/bin/opensre" in direct_module._SERVICE_UNIT
assert "/opt/opensre/.venv" not in direct_module._SERVICE_UNIT
def test_deploy_direct_raises_on_install_failure(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""deploy_direct() must raise RuntimeError when the install SSM command fails."""
monkeypatch.setattr(direct_module, "_OUTPUTS_DIR", tmp_path)
_stub_deploy(monkeypatch)
monkeypatch.setattr(
direct_module,
"run_ssm_shell_command",
lambda *_a, **_kw: {"status": "Failed", "stdout": "", "stderr": "curl error"},
)
with pytest.raises(RuntimeError, match="Install commands failed"):
direct_module.deploy_direct(env_vars={}, region="us-east-1")
def test_destroy_direct_terminates_instance_and_cleans_iam(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""destroy_direct() must terminate the instance and delete the IAM profile."""
monkeypatch.setattr(direct_module, "_OUTPUTS_DIR", tmp_path)
stack_name = direct_module._direct_stack_name()
outputs_file = tmp_path / f"{stack_name}.json"
outputs_file.write_text(
json.dumps(
{
"InstanceId": _INSTANCE_ID,
"ProfileName": "opensre-gateway-direct-profile",
"RoleName": "opensre-gateway-direct-role",
}
)
)
terminated: list[str] = []
deleted_profiles: list[str] = []
monkeypatch.setattr(
direct_module,
"terminate_instance",
lambda instance_id, _region: terminated.append(instance_id),
)
monkeypatch.setattr(
direct_module,
"delete_instance_profile",
lambda profile, _role, _region: deleted_profiles.append(profile),
)
results = direct_module.destroy_direct(region="us-east-1")
assert _INSTANCE_ID in terminated
assert deleted_profiles == ["opensre-gateway-direct-profile"]
assert any("ec2-instance" in r for r in results["deleted"])
assert not outputs_file.exists()
def test_destroy_direct_handles_missing_outputs(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""destroy_direct() must not crash when no outputs file exists."""
monkeypatch.setattr(direct_module, "_OUTPUTS_DIR", tmp_path)
monkeypatch.setattr(direct_module, "terminate_instance", lambda *_a, **_kw: None)
monkeypatch.setattr(direct_module, "delete_instance_profile", lambda *_a, **_kw: None)
results = direct_module.destroy_direct(region="us-east-1")
assert isinstance(results["deleted"], list)
assert isinstance(results["failed"], list)
def test_cleanup_existing_cleans_iam_when_no_outputs_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""_cleanup_existing() must delete the IAM profile/role even when no outputs file exists.
Regression: tagged instances were terminated but the outputs file was absent,
so destroy_direct() was skipped and IAM resources were silently orphaned.
"""
monkeypatch.setattr(direct_module, "_OUTPUTS_DIR", tmp_path)
# No outputs file — simulates a lost or never-written outputs file.
terminated: list[str] = []
deleted_profiles: list[str] = []
monkeypatch.setattr(
direct_module,
"find_stack_instance_ids",
lambda _stack, *, region: ["i-orphan01"], # noqa: ARG005
)
monkeypatch.setattr(
direct_module,
"terminate_instance",
lambda instance_id, _region: terminated.append(instance_id),
)
monkeypatch.setattr(
direct_module,
"delete_instance_profile",
lambda profile, _role, _region: deleted_profiles.append(profile),
)
ran = direct_module._cleanup_existing(region="us-east-1")
assert ran is True
assert "i-orphan01" in terminated
# IAM must be cleaned up using derived names, not skipped.
stack_name = direct_module._direct_stack_name()
assert deleted_profiles == [f"{stack_name}-profile"]
def test_stack_name_includes_suffix(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("OPENSRE_STACK_SUFFIX", "joe")
assert direct_module._direct_stack_name() == "opensre-gateway-direct-joe"
def test_stack_name_default_when_no_suffix(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("OPENSRE_STACK_SUFFIX", raising=False)
assert direct_module._direct_stack_name() == "opensre-gateway-direct"
def test_curl_install_commands_structure() -> None:
"""_build_curl_install_commands() must contain all required setup steps."""
commands = direct_module._build_curl_install_commands()
joined = "\n".join(commands)
assert "set -eu" in joined
assert "useradd" in joined # creates opensre user
assert direct_module._INSTALL_URL in joined # curl installer URL
assert "/usr/local/bin" in joined # install dir
assert "export HOME=/root" in joined # SSM has no HOME set
assert "OPENSRE_AUTO_LAUNCH=0" in joined # no interactive wizard
assert "systemctl enable opensre-gateway" in joined # service enabled
assert "opensre --help" in joined # smoke-check