SteamInstaller defaults to steamcmd="steamcmd" (bare name), which relies on PATH lookup. Deployments that don't have steamcmd on PATH — or where steamcmd.sh's `cd "$(dirname "$0")"` breaks under PATH-symlink invocation (observed with the Valve-shipped script) — can now pin an absolute path via LEFT4ME_STEAMCMD. Default keeps bare-name lookup for dev/tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
3.3 KiB
Python
110 lines
3.3 KiB
Python
import subprocess
|
|
from types import SimpleNamespace
|
|
import json
|
|
|
|
from typer.testing import CliRunner
|
|
|
|
from l4d2host.cli import app
|
|
|
|
|
|
def test_help_lists_v1_commands() -> None:
|
|
result = CliRunner().invoke(app, ["--help"])
|
|
assert result.exit_code == 0
|
|
for command in ["install", "initialize", "start", "stop", "delete"]:
|
|
assert command in result.output
|
|
|
|
|
|
def test_install_uses_left4me_steamcmd_env_var(monkeypatch) -> None:
|
|
captured: dict[str, str] = {}
|
|
|
|
class FakeInstaller:
|
|
def __init__(self, *, steamcmd):
|
|
captured["steamcmd"] = steamcmd
|
|
|
|
def install_or_update(self, **kwargs):
|
|
del kwargs
|
|
|
|
monkeypatch.setattr("l4d2host.cli.SteamInstaller", FakeInstaller)
|
|
monkeypatch.setenv("LEFT4ME_STEAMCMD", "/opt/left4me/steam/steamcmd.sh")
|
|
|
|
result = CliRunner().invoke(app, ["install"])
|
|
|
|
assert result.exit_code == 0
|
|
assert captured["steamcmd"] == "/opt/left4me/steam/steamcmd.sh"
|
|
|
|
|
|
def test_install_defaults_to_bare_steamcmd_when_env_unset(monkeypatch) -> None:
|
|
captured: dict[str, str] = {}
|
|
|
|
class FakeInstaller:
|
|
def __init__(self, *, steamcmd):
|
|
captured["steamcmd"] = steamcmd
|
|
|
|
def install_or_update(self, **kwargs):
|
|
del kwargs
|
|
|
|
monkeypatch.setattr("l4d2host.cli.SteamInstaller", FakeInstaller)
|
|
monkeypatch.delenv("LEFT4ME_STEAMCMD", raising=False)
|
|
|
|
result = CliRunner().invoke(app, ["install"])
|
|
|
|
assert result.exit_code == 0
|
|
assert captured["steamcmd"] == "steamcmd"
|
|
|
|
|
|
def test_cli_propagates_subprocess_return_code(monkeypatch) -> None:
|
|
def fail(*args, **kwargs):
|
|
del args
|
|
del kwargs
|
|
raise subprocess.CalledProcessError(returncode=9, cmd=["x"], stderr="boom")
|
|
|
|
monkeypatch.setattr("l4d2host.cli.start_instance", fail)
|
|
|
|
result = CliRunner().invoke(app, ["start", "alpha"])
|
|
|
|
assert result.exit_code == 9
|
|
assert "boom" in result.stderr
|
|
|
|
|
|
def test_status_command_outputs_json(monkeypatch) -> None:
|
|
monkeypatch.setattr(
|
|
"l4d2host.cli.get_instance_status",
|
|
lambda name: SimpleNamespace(state="running", raw_active_state="active", raw_sub_state="running"),
|
|
raising=False,
|
|
)
|
|
|
|
result = CliRunner().invoke(app, ["status", "alpha", "--json"])
|
|
|
|
assert result.exit_code == 0
|
|
assert json.loads(result.output) == {
|
|
"state": "running",
|
|
"raw_active_state": "active",
|
|
"raw_sub_state": "running",
|
|
}
|
|
|
|
|
|
def test_logs_command_streams_lines(monkeypatch) -> None:
|
|
monkeypatch.setattr(
|
|
"l4d2host.cli.stream_instance_logs",
|
|
lambda name, *, lines, follow: iter([f"{name}:{lines}:{follow}", "ready"]),
|
|
raising=False,
|
|
)
|
|
|
|
result = CliRunner().invoke(app, ["logs", "alpha", "--lines", "25", "--no-follow"])
|
|
|
|
assert result.exit_code == 0
|
|
assert result.output.splitlines() == ["alpha:25:False", "ready"]
|
|
|
|
|
|
def test_logs_command_propagates_subprocess_return_code(monkeypatch) -> None:
|
|
def fail_logs(*args, **kwargs):
|
|
del args
|
|
del kwargs
|
|
raise subprocess.CalledProcessError(returncode=7, cmd=["logs"], stderr="sudo denied")
|
|
|
|
monkeypatch.setattr("l4d2host.cli.stream_instance_logs", fail_logs, raising=False)
|
|
|
|
result = CliRunner().invoke(app, ["logs", "alpha", "--no-follow"])
|
|
|
|
assert result.exit_code == 7
|
|
assert "sudo denied" in result.stderr
|