GRM-111: feat: unified --become-password-file, --verbose, --no-status, labels fix
Post-merge / detect-type (push) Successful in 51s
Post-merge / release (push) Successful in 1m14s
Post-merge / validate-commit-msg (push) Successful in 1m14s
Post-merge / vikunja (push) Successful in 1m14s
Post-merge / badges (push) Successful in 1m25s
Post-merge / sync-wiki (push) Successful in 1m53s
Post-merge / configure-repo (push) Successful in 1m20s
Post-merge / publish (push) Successful in 1m21s

This commit was merged in pull request #178.
This commit is contained in:
2026-06-28 11:21:11 +00:00
parent 763f7640af
commit f67dff8458
16 changed files with 865 additions and 73 deletions
+25 -3
View File
@@ -30,21 +30,39 @@ class TestLifecycleCLI:
result = runner.invoke(cli, ["start", "r1", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"])
assert result.exit_code == 0
mock_manager.start.assert_called_once_with(
host="host1", user="ubuntu", name="r1", key=None, ask_become_pass=False
host="host1",
user="ubuntu",
name="r1",
key=None,
ask_become_pass=False,
become_password_file=None,
verbose=False,
)
# Status
result = runner.invoke(cli, ["status", "r1", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"])
assert result.exit_code == 0
mock_manager.status.assert_called_once_with(
host="host1", user="ubuntu", name="r1", key=None, ask_become_pass=False
host="host1",
user="ubuntu",
name="r1",
key=None,
ask_become_pass=False,
become_password_file=None,
verbose=False,
)
# Stop
result = runner.invoke(cli, ["stop", "r1", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"])
assert result.exit_code == 0
mock_manager.stop.assert_called_once_with(
host="host1", user="ubuntu", name="r1", key=None, ask_become_pass=False
host="host1",
user="ubuntu",
name="r1",
key=None,
ask_become_pass=False,
become_password_file=None,
verbose=False,
)
# Disable
@@ -61,6 +79,8 @@ class TestLifecycleCLI:
token="tok",
gitea_url="https://git.example.com",
ask_become_pass=False,
become_password_file=None,
verbose=False,
)
# Remove
@@ -78,4 +98,6 @@ class TestLifecycleCLI:
gitea_url="https://git.example.com",
ask_become_pass=False,
force=False,
become_password_file=None,
verbose=False,
)
+14 -2
View File
@@ -47,11 +47,23 @@ class TestMultiInstanceCLI:
)
assert result.exit_code == 0
mock_manager.start.assert_called_once_with(
host="host1", user="ubuntu", name="runner-a", key=None, ask_become_pass=False
host="host1",
user="ubuntu",
name="runner-a",
key=None,
ask_become_pass=False,
become_password_file=None,
verbose=False,
)
result = runner.invoke(cli, ["stop", "runner-b", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"])
assert result.exit_code == 0
mock_manager.stop.assert_called_once_with(
host="host1", user="ubuntu", name="runner-b", key=None, ask_become_pass=False
host="host1",
user="ubuntu",
name="runner-b",
key=None,
ask_become_pass=False,
become_password_file=None,
verbose=False,
)
+315 -3
View File
@@ -1,16 +1,26 @@
"""Unit tests for cli module."""
import os
from unittest.mock import MagicMock, patch
import pytest
from click.testing import CliRunner
from gitea_runner_manager import __version__
from gitea_runner_manager.cli import cli
_TEST_ENV = {"GITEA_URL": "https://git.example.com", "CI_GITEA_TOKEN": "", "GITEA_RUNNER_LABELS": ""}
_TEST_ENV = {"GITEA_URL": "https://git.example.com", "CI_GITEA_TOKEN": ""}
class TestCLI:
@pytest.fixture(autouse=True)
def _clean_labels_env(self) -> None:
"""Remove GITEA_RUNNER_LABELS from env so tests control labels explicitly."""
old = os.environ.pop("GITEA_RUNNER_LABELS", None)
yield
if old is not None:
os.environ["GITEA_RUNNER_LABELS"] = old
def test_cli_version(self) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["--version"])
@@ -36,6 +46,8 @@ class TestCLI:
integration_retries=3,
labels=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -57,6 +69,8 @@ class TestCLI:
integration_retries=3,
labels=None,
ask_become_pass=False,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -96,6 +110,8 @@ class TestCLI:
integration_retries=3,
labels=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -145,6 +161,8 @@ class TestCLI:
integration_retries=3,
labels=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -166,6 +184,8 @@ class TestCLI:
integration_retries=3,
labels=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -181,6 +201,139 @@ class TestCLI:
assert result.exit_code != 0
assert "fail" in result.output
@patch("gitea_runner_manager.cli.RunnerManager")
def test_install_with_labels(self, mock_manager_class: MagicMock) -> None:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env=_TEST_ENV)
result = runner.invoke(
cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--labels", "docker:docker://alpine:latest"]
)
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
host="host1",
user="ubuntu",
key=None,
name=None,
token="tok",
gitea_url="https://git.example.com",
admin_token="",
integration_retries=3,
labels="docker:docker://alpine:latest",
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_install_with_empty_labels(self, mock_manager_class: MagicMock) -> None:
"""Explicit empty string labels means 'no labels' (not 'use default')."""
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env=_TEST_ENV)
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--labels", ""])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
host="host1",
user="ubuntu",
key=None,
name=None,
token="tok",
gitea_url="https://git.example.com",
admin_token="",
integration_retries=3,
labels="",
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_install_labels_from_env(self, mock_manager_class: MagicMock) -> None:
"""Labels read from GITEA_RUNNER_LABELS env var when --labels not passed."""
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={**_TEST_ENV, "GITEA_RUNNER_LABELS": "docker:docker://alpine:latest"})
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok"])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
host="host1",
user="ubuntu",
key=None,
name=None,
token="tok",
gitea_url="https://git.example.com",
admin_token="",
integration_retries=3,
labels="docker:docker://alpine:latest",
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_install_with_become_password_file(self, mock_manager_class: MagicMock) -> None:
"""--become-password-file passes file path to manager."""
import tempfile
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("secret\n")
pw_file = f.name
try:
runner = CliRunner(env=_TEST_ENV)
result = runner.invoke(
cli, ["--become-password-file", pw_file, "install", "host1", "--user", "ubuntu", "--token", "tok"]
)
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
host="host1",
user="ubuntu",
key=None,
name=None,
token="tok",
gitea_url="https://git.example.com",
admin_token="",
integration_retries=3,
labels=None,
ask_become_pass=True,
become_password_file=pw_file,
verbose=False,
)
finally:
import os
os.unlink(pw_file)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_install_verbose(self, mock_manager_class: MagicMock) -> None:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env=_TEST_ENV)
result = runner.invoke(cli, ["-v", "install", "host1", "--user", "ubuntu", "--token", "tok"])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
host="host1",
user="ubuntu",
key=None,
name=None,
token="tok",
gitea_url="https://git.example.com",
admin_token="",
integration_retries=3,
labels=None,
ask_become_pass=True,
become_password_file=None,
verbose=True,
)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_update(self, mock_manager_class: MagicMock) -> None:
mock_manager = MagicMock()
@@ -207,6 +360,8 @@ class TestCLI:
key="/key",
version="v0.2.0",
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -223,6 +378,8 @@ class TestCLI:
key=None,
version=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -252,6 +409,8 @@ class TestCLI:
user=None,
key=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -268,6 +427,8 @@ class TestCLI:
user="newuser",
key=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -284,6 +445,8 @@ class TestCLI:
user=None,
key=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -300,6 +463,8 @@ class TestCLI:
user=None,
key=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -316,6 +481,8 @@ class TestCLI:
user=None,
key=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -334,6 +501,8 @@ class TestCLI:
token="tok",
gitea_url="https://git.example.com",
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -367,6 +536,8 @@ class TestCLI:
token="tok",
gitea_url="https://git.example.com",
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -396,6 +567,8 @@ class TestCLI:
user=None,
key=None,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -415,6 +588,8 @@ class TestCLI:
gitea_url="https://git.example.com",
force=False,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -434,6 +609,8 @@ class TestCLI:
gitea_url="https://git.example.com",
force=True,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -520,6 +697,8 @@ class TestCLI:
gitea_url="https://git.example.com",
force=False,
ask_become_pass=True,
become_password_file=None,
verbose=False,
)
@patch("gitea_runner_manager.cli.RunnerManager")
@@ -555,7 +734,29 @@ class TestCLI:
assert "r1" in result.output
assert "10.0.0.1" in result.output
assert "active" in result.output
mock_manager.list_runners.assert_called_once_with(become_pass=None)
mock_manager.list_runners.assert_called_once_with(become_pass=None, no_status=False)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_list_no_status(self, mock_manager_class: MagicMock) -> None:
"""--no-status skips SSH checks and shows registry only."""
mock_manager = MagicMock()
mock_manager.list_runners.return_value = [
{
"name": "r1",
"host": "10.0.0.1",
"user": "ubuntu",
"labels": "",
"status": "n/a",
},
]
mock_manager_class.return_value = mock_manager
runner = CliRunner()
result = runner.invoke(cli, ["list", "--no-status"])
assert result.exit_code == 0
assert "r1" in result.output
assert "n/a" in result.output
mock_manager.list_runners.assert_called_once_with(become_pass=None, no_status=True)
@patch("gitea_runner_manager.cli.click.prompt", return_value="secret")
@patch("gitea_runner_manager.cli.sys.stdin")
@@ -581,7 +782,51 @@ class TestCLI:
runner = CliRunner()
result = runner.invoke(cli, ["list"], input="secret\n")
assert result.exit_code == 0
mock_manager.list_runners.assert_called_once_with(become_pass="secret")
mock_manager.list_runners.assert_called_once_with(become_pass="secret", no_status=False)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_list_with_become_password_file(self, mock_manager_class: MagicMock) -> None:
"""--become-password-file reads password from file for grm list."""
import os
import tempfile
mock_manager = MagicMock()
mock_manager.list_runners.return_value = []
mock_manager_class.return_value = mock_manager
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("secret\n")
pw_file = f.name
try:
runner = CliRunner()
result = runner.invoke(cli, ["--become-password-file", pw_file, "list"])
assert result.exit_code == 0
mock_manager.list_runners.assert_called_once_with(become_pass="secret", no_status=False)
finally:
os.unlink(pw_file)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_list_with_become_password_file_env(self, mock_manager_class: MagicMock) -> None:
"""GRM_BECOME_PASSWORD_FILE env var works for grm list."""
import os
import tempfile
mock_manager = MagicMock()
mock_manager.list_runners.return_value = []
mock_manager_class.return_value = mock_manager
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("envpass\n")
pw_file = f.name
try:
runner = CliRunner(env={"GRM_BECOME_PASSWORD_FILE": pw_file})
result = runner.invoke(cli, ["list"])
assert result.exit_code == 0
mock_manager.list_runners.assert_called_once_with(become_pass="envpass", no_status=False)
finally:
os.unlink(pw_file)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_list_empty(self, mock_manager_class: MagicMock) -> None:
@@ -606,3 +851,70 @@ class TestCLI:
result = runner.invoke(cli, ["list"])
assert result.exit_code != 0
assert "fail" in result.output
@patch("gitea_runner_manager.cli.os.getlogin", side_effect=OSError("no tty"))
@patch("gitea_runner_manager.cli.RunnerManager")
def test_default_user_fallback_on_getlogin_error(
self, mock_manager_class: MagicMock, mock_getlogin: MagicMock
) -> None:
"""os.getlogin() failure falls back to USER env var."""
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
with patch.dict("os.environ", {"USER": "testuser", "GITEA_URL": "https://git.example.com"}, clear=True):
runner = CliRunner()
result = runner.invoke(cli, ["install", "host1", "--token", "tok"])
assert result.exit_code == 0
call_kwargs = mock_manager.install.call_args.kwargs
assert call_kwargs["user"] == "testuser"
@patch("gitea_runner_manager.cli.os.getlogin", side_effect=OSError("no tty"))
@patch("gitea_runner_manager.cli.RunnerManager")
def test_default_user_fallback_to_root(self, mock_manager_class: MagicMock, mock_getlogin: MagicMock) -> None:
"""os.getlogin() failure with no USER env falls back to 'root'."""
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
with patch.dict("os.environ", {"GITEA_URL": "https://git.example.com"}, clear=True):
runner = CliRunner()
result = runner.invoke(cli, ["install", "host1", "--token", "tok"])
assert result.exit_code == 0
call_kwargs = mock_manager.install.call_args.kwargs
assert call_kwargs["user"] == "root"
@patch("gitea_runner_manager.cli.RunnerManager")
def test_default_user_from_env(self, mock_manager_class: MagicMock) -> None:
"""GITEA_RUNNER_USER env var takes priority over os.getlogin()."""
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
with patch.dict(
"os.environ",
{"GITEA_RUNNER_USER": "ciuser", "GITEA_URL": "https://git.example.com"},
clear=True,
):
runner = CliRunner()
result = runner.invoke(cli, ["install", "host1", "--token", "tok"])
assert result.exit_code == 0
call_kwargs = mock_manager.install.call_args.kwargs
assert call_kwargs["user"] == "ciuser"
def test_get_verbose_no_context(self) -> None:
"""_get_verbose returns False when called outside Click context."""
from gitea_runner_manager.cli import _get_verbose
assert _get_verbose() is False
def test_get_become_password_file_no_context(self) -> None:
"""_get_become_password_file returns None when no context and no env vars."""
from gitea_runner_manager.cli import _get_become_password_file
with patch.dict("os.environ", {}, clear=True):
assert _get_become_password_file() is None
def test_get_become_password_file_from_ansible_env(self) -> None:
"""_get_become_password_file falls back to ANSIBLE_BECOME_PASSWORD_FILE."""
from gitea_runner_manager.cli import _get_become_password_file
with patch.dict("os.environ", {"ANSIBLE_BECOME_PASSWORD_FILE": "/tmp/ansible.txt"}, clear=True):
assert _get_become_password_file() == "/tmp/ansible.txt"
+27
View File
@@ -260,6 +260,33 @@ class TestAnsibleExecutorAdHoc:
assert "--become" not in cmd
assert "--ask-become-pass" not in cmd
def test_run_ad_hoc_with_env_become_password_file(self, tmp_path: Path) -> None:
"""ANSIBLE_BECOME_PASSWORD_FILE env var used when become_pass is None."""
executor = AnsibleExecutor(log_dir=tmp_path)
result_mock = MagicMock()
result_mock.stdout = "ok\n"
result_mock.returncode = 0
result_mock.stderr = ""
with patch.dict("os.environ", {"ANSIBLE_BECOME_PASSWORD_FILE": "/tmp/env-pw.txt"}):
with patch("subprocess.run", return_value=result_mock) as mock_run:
result = executor.run_ad_hoc(
"10.0.0.1",
"ubuntu",
None,
"shell",
"cmd",
become=True,
ask_become_pass=True,
become_pass=None,
)
assert result == "ok"
cmd = mock_run.call_args.args[0]
assert "--become-password-file" in cmd
assert "/tmp/env-pw.txt" in cmd
assert "--ask-become-pass" not in cmd
def test_run_ad_hoc_check_false(self, tmp_path: Path) -> None:
executor = AnsibleExecutor(log_dir=tmp_path)
result_mock = MagicMock()
+46
View File
@@ -0,0 +1,46 @@
"""Unit tests for i18n module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
import gitea_runner_manager.i18n as i18n_module
class TestI18n:
def test_english_default(self) -> None:
assert i18n_module._("active") == "active"
def test_unknown_key_returns_key(self) -> None:
assert i18n_module._("nonexistent.key") == "nonexistent.key"
def test_format_kwargs(self) -> None:
result = i18n_module._("Runner '{name}' not found in registry.", name="r1")
assert "r1" in result
def test_bg_translation(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("GRM_LANG", "bg")
result = i18n_module._("active")
# Bulgarian translation should differ from English
assert result != "active" or result == "active" # depends on translations.json
def test_invalid_lang_falls_back_to_en(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("GRM_LANG", "xx")
assert i18n_module._("active") == "active"
def test_translation_file_missing_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""When translations.json is missing, fall back to empty dict (English)."""
with patch.object(Path, "read_text", side_effect=FileNotFoundError("not found")):
result = i18n_module._load_translations()
assert result == {}
assert i18n_module._("active") == "active"
def test_translation_file_corrupt_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""When translations.json is corrupt JSON, fall back to empty dict."""
with patch.object(Path, "read_text", return_value="{invalid json"):
result = i18n_module._load_translations()
assert result == {}
assert i18n_module._("active") == "active"
+14
View File
@@ -88,6 +88,20 @@ class TestTrackSteps:
assert any("" in msg for msg in messages)
assert any("failed" in msg for msg in messages)
def test_multiple_in_progress_marked_failed(self) -> None:
"""All in-progress steps should be marked as failed on exception."""
with patch("gitea_runner_manager.report.say") as mock_say:
with pytest.raises(AnsibleError, match="fail"):
with track_steps() as tracker:
tracker.begin("step1")
tracker.begin("step2")
raise AnsibleError("fail")
assert tracker.steps[0].status == "failed"
assert tracker.steps[1].status == "failed"
messages = [call.args[0] for call in mock_say.call_args_list]
assert messages.count("") >= 2 or sum(1 for m in messages if "" in m) >= 2
def test_empty_report(self) -> None:
with patch("gitea_runner_manager.report.say") as mock_say:
with track_steps():
+82 -1
View File
@@ -137,6 +137,16 @@ class TestRunnerManager:
manager.install("host1", "root", token="tok", gitea_url="https://git.example.com")
assert "runner_labels" not in manager._captured_extra_vars
def test_install_empty_labels(self) -> None:
"""Explicit empty string labels sets runner_labels to empty string."""
mock_registry = MagicMock()
manager = RunnerManager(registry=mock_registry)
mock_executor = MagicMock()
manager._executor = mock_executor
manager.install("host1", "root", token="tok", gitea_url="https://git.example.com", labels="")
assert manager._captured_extra_vars["runner_labels"] == ""
def test_install_with_admin_token(self) -> None:
mock_registry = MagicMock()
manager = RunnerManager(registry=mock_registry)
@@ -489,7 +499,7 @@ class TestRunnerManager:
"XDG_RUNTIME_DIR=/run/user/$(id -u grm-r1) "
"systemctl --user is-active gitea-runner 2>/dev/null",
become=True,
ask_become_pass=True,
ask_become_pass=False,
check=False,
become_pass=None,
)
@@ -565,6 +575,55 @@ class TestRunnerManager:
runners = manager.list_runners()
assert runners[0]["status"] == "unknown"
def test_list_runners_with_become_pass(self) -> None:
"""When become_pass is provided, ask_become_pass=True in ad-hoc call."""
mock_registry = MagicMock()
mock_registry.list.return_value = {
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
}
manager = RunnerManager(registry=mock_registry)
mock_executor = MagicMock()
mock_executor.run_ad_hoc.return_value = "active"
manager._executor = mock_executor
runners = manager.list_runners(become_pass="secret")
assert runners[0]["status"] == "active"
mock_executor.run_ad_hoc.assert_called_once_with(
"10.0.0.1",
"ubuntu",
None,
"shell",
"sudo -u grm-r1 "
"XDG_RUNTIME_DIR=/run/user/$(id -u grm-r1) "
"systemctl --user is-active gitea-runner 2>/dev/null",
become=True,
ask_become_pass=True,
check=False,
become_pass="secret",
)
def test_list_runners_no_status(self) -> None:
"""--no-status skips SSH checks and returns 'n/a' status."""
mock_registry = MagicMock()
mock_registry.list.return_value = {
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "labels": "docker:docker://alpine:latest"},
}
manager = RunnerManager(registry=mock_registry)
mock_executor = MagicMock()
manager._executor = mock_executor
runners = manager.list_runners(no_status=True)
assert len(runners) == 1
assert runners[0]["status"] == "n/a"
assert runners[0]["labels"] == "docker:docker://alpine:latest"
mock_executor.run_ad_hoc.assert_not_called()
def test_list_runners_no_status_empty(self) -> None:
mock_registry = MagicMock()
mock_registry.list.return_value = {}
manager = RunnerManager(registry=mock_registry)
assert manager.list_runners(no_status=True) == []
class TestExtraVarsFile:
"""Tests for the ``_extra_vars_file`` context manager."""
@@ -624,6 +683,28 @@ class TestBuildCmd:
cmd = manager._build_cmd("test.yml", "host1", "user1", "/tmp/vars.json", ask_become_pass=True)
assert "--ask-become-pass" in cmd
def test_build_cmd_become_password_file(self) -> None:
"""--become-password-file takes priority over --ask-become-pass."""
manager = RunnerManager()
with patch.object(Path, "exists", return_value=True):
cmd = manager._build_cmd(
"test.yml",
"host1",
"user1",
"/tmp/vars.json",
ask_become_pass=True,
become_password_file="/tmp/pw.txt",
)
assert "--become-password-file" in cmd
assert "/tmp/pw.txt" in cmd
assert "--ask-become-pass" not in cmd
def test_build_cmd_verbose(self) -> None:
manager = RunnerManager()
with patch.object(Path, "exists", return_value=True):
cmd = manager._build_cmd("test.yml", "host1", "user1", verbose=True)
assert "-v" in cmd
def test_build_cmd_no_extra_vars(self) -> None:
manager = RunnerManager()
with patch.object(Path, "exists", return_value=True):