Files
algo/tests/unit/test_list_servers.py
T
5a72268117 feat: add destroy subcommand to tear down deployed servers (#14965)
* feat: add destroy subcommand to tear down deployed servers

Add `./algo destroy <server-ip>` to programmatically remove cloud
resources and clean up local configs. Reads provider and server name
from configs/<ip>/.config.yml, gathers credentials via existing
prompts.yml, confirms with user, then dispatches to provider-specific
destroy tasks.

Supports all 11 cloud providers:
- DigitalOcean, EC2, Lightsail (CloudFormation), Azure (resource group),
  GCE (instance + subsidiary resources), Hetzner, Vultr, Scaleway,
  OpenStack, CloudStack, Linode
- Local provider: config cleanup only

Also stores algo_region in .config.yml during deployment so destroy
can auto-detect region. Fixes Scaleway module to allow state=absent
without image/commercial_type/organization params. Adds Vultr to
region-required providers and stores algo_region in Vultr prompts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add list-servers script and tests

Add scripts/list_servers.py to scan configs/ for deployed server
metadata and output JSON. Referenced by `./algo list-servers`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 13:24:14 -05:00

96 lines
2.7 KiB
Python

"""Tests for scripts/list_servers.py."""
import importlib.util
import json
import subprocess
import sys
from pathlib import Path
import pytest
# Load list_servers module from scripts/ (not a Python package)
_script = Path(__file__).resolve().parents[2] / "scripts" / "list_servers.py"
_spec = importlib.util.spec_from_file_location("list_servers", str(_script))
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
list_servers = _mod.list_servers
@pytest.fixture()
def configs_dir(tmp_path):
"""Create a temporary configs directory with sample configs."""
server1 = tmp_path / "10.0.0.1"
server1.mkdir()
(server1 / ".config.yml").write_text("server: 10.0.0.1\nalgo_provider: digitalocean\nalgo_server_name: algo\n")
server2 = tmp_path / "10.0.0.2"
server2.mkdir()
(server2 / ".config.yml").write_text("server: 10.0.0.2\nalgo_provider: ec2\nalgo_server_name: prod\n")
return tmp_path
def test_empty_directory(tmp_path):
"""Empty configs directory returns empty list."""
assert list_servers(tmp_path) == []
def test_missing_directory(tmp_path):
"""Non-existent path returns empty list via glob."""
assert list_servers(tmp_path / "nonexistent") == []
def test_lists_servers(configs_dir):
"""Parses .config.yml files and returns server metadata."""
servers = list_servers(configs_dir)
assert len(servers) == 2
names = {s["algo_server_name"] for s in servers}
assert names == {"algo", "prod"}
def test_sorted_output(configs_dir):
"""Servers are returned in sorted directory order."""
servers = list_servers(configs_dir)
ips = [s["server"] for s in servers]
assert ips == ["10.0.0.1", "10.0.0.2"]
def test_skips_empty_yaml(tmp_path):
"""Empty YAML files (parsing to None) are skipped."""
server = tmp_path / "10.0.0.5"
server.mkdir()
(server / ".config.yml").write_text("")
assert list_servers(tmp_path) == []
def test_cli_output(tmp_path):
"""CLI outputs valid JSON to stdout."""
server = tmp_path / "10.0.0.1"
server.mkdir()
(server / ".config.yml").write_text("server: 10.0.0.1\nalgo_server_name: test\n")
result = subprocess.run(
[sys.executable, "scripts/list_servers.py", str(tmp_path)],
capture_output=True,
text=True,
check=True,
)
data = json.loads(result.stdout)
assert len(data) == 1
assert data[0]["server"] == "10.0.0.1"
def test_cli_missing_dir():
"""CLI outputs empty JSON array for missing directory."""
result = subprocess.run(
[
sys.executable,
"scripts/list_servers.py",
"/nonexistent/path",
],
capture_output=True,
text=True,
check=True,
)
assert json.loads(result.stdout) == []