import subprocess
import json
import builtins
import hashlib
import os
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock

import click
from typer.testing import CliRunner

from armorer.main import (
    _maybe_offer_openclaw_gateway_background,
    _openclaw_gateway_exec_override,
    _maybe_apply_openclaw_setup_defaults,
    _claim_telegram_gateway_singleton,
    _core_provider_env,
    _ensure_host_core_process,
    _ensure_openclaw_gateway_allowed_origins,
    _ensure_openclaw_runtime_defaults,
    _ensure_startup_services_ready,
    _ensure_telegram_gateway_autostart,
    _wait_openclaw_gateway_reachable,
    _check_notifications,
    run as run_command,
    app,
)
from armorer.registry import AppMetadata
from rich.panel import Panel

runner = CliRunner()


def test_ensure_host_core_process_reuses_live_core_without_spawning(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)

    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: config_dir)
    monkeypatch.setattr("armorer.main._list_running_host_core_pids", lambda exclude_pid=None: [24680])

    def _unexpected_popen(*args, **kwargs):
        raise AssertionError("host core should not be spawned when an instance is already running")

    monkeypatch.setattr("armorer.main.subprocess.Popen", _unexpected_popen)

    assert _ensure_host_core_process() is True
    assert (config_dir / "core-host.pid").read_text().strip() == "24680"


def test_ensure_telegram_gateway_autostart_reuses_live_gateway_without_spawning(tmp_path: Path, monkeypatch):
    printed: list[str] = []
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)

    def _fake_get_key(key: str):
        values = {
            "ARMORER_GATEWAY_TELEGRAM_ENABLED": "true",
            "TELEGRAM_BOT_TOKEN": "token-123",
        }
        return values.get(key)

    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: config_dir)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: SimpleNamespace(get_key=_fake_get_key))
    monkeypatch.setattr("armorer.main._list_running_telegram_gateway_pids", lambda exclude_pid=None: [43210])
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.extend(str(arg) for arg in args))

    def _unexpected_popen(*args, **kwargs):
        raise AssertionError("telegram gateway should not be spawned when an instance is already running")

    monkeypatch.setattr("armorer.main.subprocess.Popen", _unexpected_popen)

    _ensure_telegram_gateway_autostart()

    assert (config_dir / "gateway-telegram.pid").read_text().strip() == "43210"
    assert all("auto-started" not in entry for entry in printed)


def test_gateway_telegram_claims_and_releases_pid_file(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)

    def _fake_get_key(key: str):
        values = {
            "ARMORER_TASK_SIGNING_KEY": "signing-key",
            "TELEGRAM_BOT_TOKEN": "token-123",
        }
        return values.get(key)

    class _FakeBridge:
        def __init__(self, gateway, relay, response_timeout):
            self.gateway = gateway
            self.relay = relay
            self.response_timeout = response_timeout

        def run_forever(self, poll_timeout: int):
            pid_file = config_dir / "gateway-telegram.pid"
            assert pid_file.read_text().strip() == str(os.getpid())
            raise KeyboardInterrupt

    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: config_dir)
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: SimpleNamespace(get_key=_fake_get_key))
    monkeypatch.setattr("armorer.main._list_running_telegram_gateway_pids", lambda exclude_pid=None: [])
    monkeypatch.setattr("armorer.main.create_messaging_gateway", lambda *args, **kwargs: object())
    monkeypatch.setattr("armorer.main.InboxOutboxRelay", lambda **kwargs: object())
    monkeypatch.setattr("armorer.main.TelegramCoreBridge", _FakeBridge)

    result = runner.invoke(app, ["gateway", "telegram"])

    assert result.exit_code == 0
    assert not (config_dir / "gateway-telegram.pid").exists()
    assert "Telegram gateway active" in result.stdout
    assert "Telegram gateway stopped" in result.stdout


def test_gateway_telegram_exits_when_another_gateway_is_running(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)

    def _fake_get_key(key: str):
        values = {
            "ARMORER_TASK_SIGNING_KEY": "signing-key",
            "TELEGRAM_BOT_TOKEN": "token-123",
        }
        return values.get(key)

    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: config_dir)
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: SimpleNamespace(get_key=_fake_get_key))
    monkeypatch.setattr("armorer.main._list_running_telegram_gateway_pids", lambda exclude_pid=None: [98765])

    def _unexpected_gateway(*args, **kwargs):
        raise AssertionError("duplicate telegram gateway should not be created")

    monkeypatch.setattr("armorer.main.create_messaging_gateway", _unexpected_gateway)

    result = runner.invoke(app, ["gateway", "telegram"])

    assert result.exit_code == 0
    assert "already active (PID 98765)" in result.stdout
    assert (config_dir / "gateway-telegram.pid").read_text().strip() == "98765"


def test_claim_telegram_gateway_singleton_reuses_existing_running_pid(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)
    pid_file = config_dir / "gateway-telegram.pid"
    pid_file.write_text("45678")

    monkeypatch.setattr("armorer.main._is_pid_alive", lambda pid: pid == 45678)
    monkeypatch.setattr("armorer.main._list_running_telegram_gateway_pids", lambda exclude_pid=None: [])

    claimed, active_pid = _claim_telegram_gateway_singleton(config_dir, pid=12345)

    assert claimed is False
    assert active_pid == 45678
    assert pid_file.read_text().strip() == "45678"


def test_core_provider_env_only_reads_selected_provider_key():
    calls: list[str] = []
    values = {
        "MODEL_PROVIDER": "ollama",
        "Armorer_MODEL": "armorer-qwen3.5-9b",
        "OLLAMA_BASE_URL": "http://localhost:11434/v1/",
    }

    class _FakeConfigManager:
        def get_key(self, key: str):
            calls.append(key)
            return values.get(key)

    env = _core_provider_env(_FakeConfigManager())

    assert env == {
        "MODEL_PROVIDER": "ollama",
        "Armorer_MODEL": "armorer-qwen3.5-9b",
        "OLLAMA_BASE_URL": "http://localhost:11434/v1/",
    }
    assert "OPENAI_API_KEY" not in calls
    assert "OPENROUTER_API_KEY" not in calls
    assert "ANTHROPIC_API_KEY" not in calls
    assert "GEMINI_API_KEY" not in calls


def test_check_notifications_limits_display_to_latest_unread_and_marks_all_read(tmp_path: Path, monkeypatch):
    events_file = tmp_path / "events.json"
    events = [
        {
            "type": "ALERT",
            "agent": f"agent-{idx}",
            "description": f"event-{idx}",
            "timestamp": float(idx + 1),
            "unread": True,
        }
        for idx in range(15)
    ]
    events_file.write_text(json.dumps(events))

    printed: list[object] = []
    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: tmp_path)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.extend(args))

    _check_notifications(max_display=3)

    panel_entries = [entry for entry in printed if isinstance(entry, Panel)]
    assert len(panel_entries) == 3
    panel_text = [str(panel.renderable) for panel in panel_entries]
    assert any("agent-12" in text for text in panel_text)
    assert any("agent-13" in text for text in panel_text)
    assert any("agent-14" in text for text in panel_text)
    assert all("agent-0" not in text for text in panel_text)
    assert any(
        isinstance(entry, str) and "older or duplicate unread security events suppressed" in entry
        for entry in printed
    )

    persisted = json.loads(events_file.read_text())
    assert all(not bool(event.get("unread")) for event in persisted)


def test_check_notifications_handles_missing_event_fields(tmp_path: Path, monkeypatch):
    events_file = tmp_path / "events.json"
    events_file.write_text(
        json.dumps(
            [
                {"unread": True},
                {"unread": True, "type": "notice", "timestamp": "bad-ts"},
            ]
        )
    )

    printed: list[object] = []
    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: tmp_path)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.extend(args))

    _check_notifications(max_display=5)

    panel_entries = [entry for entry in printed if isinstance(entry, Panel)]
    assert len(panel_entries) == 2
    panel_text = [str(panel.renderable) for panel in panel_entries]
    assert any("No description provided." in text and "unknown" in text for text in panel_text)
    assert any("NOTICE" in text for text in panel_text)

    persisted = json.loads(events_file.read_text())
    assert all(not bool(event.get("unread")) for event in persisted)


def test_check_notifications_uses_tmp_cache_when_events_file_cannot_be_written(tmp_path: Path, monkeypatch):
    events_file = tmp_path / "events.json"
    events_file.write_text(
        json.dumps(
            [
                {
                    "type": "guard_alert",
                    "agent": "prindleclaw",
                    "description": "repeat-me",
                    "timestamp": 10.0,
                    "unread": True,
                },
                {
                    "type": "guard_alert",
                    "agent": "prindleclaw",
                    "description": "repeat-me-newer",
                    "timestamp": 11.0,
                    "unread": True,
                },
            ]
        )
    )

    printed_first: list[object] = []
    printed_second: list[object] = []
    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: tmp_path)
    monkeypatch.setattr("armorer.main.tempfile.gettempdir", lambda: str(tmp_path))

    original_open = builtins.open

    def _fake_open(path, mode="r", *args, **kwargs):
        if Path(path) == events_file and "w" in mode:
            raise PermissionError("read-only events file")
        return original_open(path, mode, *args, **kwargs)

    monkeypatch.setattr(builtins, "open", _fake_open)

    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed_first.extend(args))
    _check_notifications(max_display=5)

    panel_first = [entry for entry in printed_first if isinstance(entry, Panel)]
    assert len(panel_first) == 2

    # Unread flags remain because write failed.
    persisted = json.loads(events_file.read_text())
    assert all(bool(event.get("unread")) for event in persisted)

    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed_second.extend(args))
    _check_notifications(max_display=5)

    panel_second = [entry for entry in printed_second if isinstance(entry, Panel)]
    assert panel_second == []


def test_check_notifications_tmp_cache_suppresses_repeat_without_timestamps_when_events_file_read_only(
    tmp_path: Path,
    monkeypatch,
):
    events_file = tmp_path / "events.json"
    events_file.write_text(
        json.dumps(
            [
                {
                    "type": "guard_alert",
                    "agent": "prindleclaw",
                    "description": "repeat-no-ts",
                    "unread": True,
                },
                {
                    "type": "guard_alert",
                    "agent": "prindleclaw",
                    "description": "repeat-no-ts-2",
                    "unread": True,
                },
            ]
        )
    )

    printed_first: list[object] = []
    printed_second: list[object] = []
    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: tmp_path)
    monkeypatch.setattr("armorer.main.tempfile.gettempdir", lambda: str(tmp_path))

    original_open = builtins.open

    def _fake_open(path, mode="r", *args, **kwargs):
        if Path(path) == events_file and "w" in mode:
            raise PermissionError("read-only events file")
        return original_open(path, mode, *args, **kwargs)

    monkeypatch.setattr(builtins, "open", _fake_open)

    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed_first.extend(args))
    _check_notifications(max_display=5)
    panel_first = [entry for entry in printed_first if isinstance(entry, Panel)]
    assert len(panel_first) == 2

    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed_second.extend(args))
    _check_notifications(max_display=5)
    panel_second = [entry for entry in printed_second if isinstance(entry, Panel)]
    assert panel_second == []


def test_check_notifications_clears_unread_flags_even_when_all_entries_are_cache_suppressed(
    tmp_path: Path,
    monkeypatch,
):
    events_file = tmp_path / "events.json"
    events = [
        {
            "type": "guard_alert",
            "agent": "prindleclaw",
            "description": "already-shown",
            "timestamp": 10.0,
            "unread": True,
        }
    ]
    events_file.write_text(json.dumps(events))

    cache_key = hashlib.sha256(str(events_file).encode("utf-8")).hexdigest()[:16]
    cache_path = tmp_path / f"armorer-notify-state-{cache_key}.json"
    event_payload = "GUARD_ALERT|prindleclaw|already-shown|10.000000"
    event_hash = hashlib.sha256(event_payload.encode("utf-8")).hexdigest()[:24]
    cache_path.write_text(
        json.dumps(
            {
                "last_shown_ts": 10.0,
                "shown_event_hashes": [event_hash],
                "persisted": False,
            }
        )
    )

    printed: list[object] = []
    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: tmp_path)
    monkeypatch.setattr("armorer.main.tempfile.gettempdir", lambda: str(tmp_path))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.extend(args))

    _check_notifications(max_display=5)

    panel_entries = [entry for entry in printed if isinstance(entry, Panel)]
    assert panel_entries == []
    persisted = json.loads(events_file.read_text())
    assert all(not bool(event.get("unread")) for event in persisted)


def test_check_notifications_tmp_cache_surfaces_new_no_timestamp_rows_once_in_read_only_mode(
    tmp_path: Path,
    monkeypatch,
):
    events_file = tmp_path / "events.json"
    events_file.write_text(
        json.dumps(
            [
                {
                    "type": "guard_alert",
                    "agent": "prindleclaw",
                    "description": "duplicate-no-ts",
                    "unread": True,
                },
                {
                    "type": "guard_alert",
                    "agent": "prindleclaw",
                    "description": "another-no-ts",
                    "unread": True,
                },
            ]
        )
    )

    printed_first: list[object] = []
    printed_second: list[object] = []
    monkeypatch.setattr("armorer.main._armorer_config_dir", lambda: tmp_path)
    monkeypatch.setattr("armorer.main.tempfile.gettempdir", lambda: str(tmp_path))

    original_open = builtins.open
    block_events_write = {"value": True}

    def _fake_open(path, mode="r", *args, **kwargs):
        if Path(path) == events_file and "w" in mode and block_events_write["value"]:
            raise PermissionError("read-only events file")
        return original_open(path, mode, *args, **kwargs)

    monkeypatch.setattr(builtins, "open", _fake_open)

    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed_first.extend(args))
    _check_notifications(max_display=5)
    panel_first = [entry for entry in printed_first if isinstance(entry, Panel)]
    assert len(panel_first) == 2

    # Simulate new event appended by another process while this process still
    # cannot clear unread flags in events.json.
    block_events_write["value"] = False
    events = json.loads(events_file.read_text())
    events.append(
        {
            "type": "guard_alert",
            "agent": "prindleclaw",
            "description": "duplicate-no-ts",
            "unread": True,
        }
    )
    events_file.write_text(json.dumps(events))
    block_events_write["value"] = True

    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed_second.extend(args))
    _check_notifications(max_display=5)
    panel_second = [entry for entry in printed_second if isinstance(entry, Panel)]
    assert len(panel_second) == 1
    assert "duplicate-no-ts" in str(panel_second[0].renderable)


def test_run_openclaw_not_found_shows_install_guidance(tmp_path: Path, monkeypatch):
    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = []

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 1
    assert "Error: Agent 'openclaw' not found." in result.stdout
    assert "install openclaw" in result.stdout


def test_stop_openclaw_not_found_shows_install_guidance(tmp_path: Path, monkeypatch):
    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = []

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)

    result = runner.invoke(app, ["stop", "openclaw"])

    assert result.exit_code == 1
    assert "Error: Agent 'openclaw' not found." in result.stdout
    assert "install openclaw" in result.stdout


def test_scan_openclaw_not_found_shows_install_guidance(tmp_path: Path, monkeypatch):
    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = []

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)

    result = runner.invoke(app, ["scan", "openclaw"])

    assert result.exit_code == 1
    assert "Error: Agent 'openclaw' not found." in result.stdout
    assert "install openclaw" in result.stdout


def test_scan_openclaw_multiple_instances_non_interactive_requires_alias(tmp_path: Path, monkeypatch):
    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = [
        AppMetadata(name="prindleclaw", install_path=str(tmp_path / "installed" / "prindleclaw"), agent_id="openclaw"),
        AppMetadata(name="openclaw_2", install_path=str(tmp_path / "installed" / "openclaw_2"), agent_id="openclaw"),
    ]

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._fallback_openclaw_default_app", lambda *args, **kwargs: None)

    result = runner.invoke(app, ["scan", "openclaw"])

    assert result.exit_code == 1
    assert "Error: Multiple instances found for 'openclaw': prindleclaw, openclaw_2." in result.stdout
    assert "Use an" in result.stdout
    assert "alias." in result.stdout


def test_run_openclaw_multiple_instances_non_interactive_requires_alias(tmp_path: Path, monkeypatch):
    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = [
        AppMetadata(name="prindleclaw", install_path=str(tmp_path / "installed" / "prindleclaw"), agent_id="openclaw"),
        AppMetadata(name="openclaw_2", install_path=str(tmp_path / "installed" / "openclaw_2"), agent_id="openclaw"),
    ]

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main._fallback_openclaw_default_app", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.sys.stdin.isatty", lambda: False, raising=False)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 1
    assert "Error: Multiple instances found for 'openclaw': prindleclaw, openclaw_2." in result.stdout
    assert "Use an" in result.stdout
    assert "alias." in result.stdout
    assert "Agent 'openclaw' not found" not in result.stdout


def test_run_openclaw_multiple_instances_interactive_selects_alias(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw_2"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    prindleclaw = AppMetadata(
        name="prindleclaw",
        install_path=str(tmp_path / "installed" / "prindleclaw"),
        agent_id="openclaw",
    )
    openclaw_2 = AppMetadata(
        name="openclaw_2",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()

    def _registry_get_app(name: str):
        if str(name).strip() == "openclaw":
            return None
        if str(name).strip() == "openclaw_2":
            return openclaw_2
        if str(name).strip() == "prindleclaw":
            return prindleclaw
        return None

    registry.get_app.side_effect = _registry_get_app
    registry.list_apps.return_value = [prindleclaw, openclaw_2]

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )
    calls: list[list[str]] = []
    selections: list[tuple[str, list[str]]] = []

    def _select(message: str, choices):
        selections.append((message, list(choices)))
        return "openclaw_2"

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.load_dotenv", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._fallback_openclaw_default_app", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", _select)
    monkeypatch.setattr("armorer.main.sys.stdin.isatty", lambda: True, raising=False)
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda cmd, **kwargs: calls.append(list(cmd)) or subprocess.CompletedProcess(cmd, 0),
    )

    try:
        run_command(SimpleNamespace(args=["status"]), "openclaw")
    except click.exceptions.Exit as exc:
        assert int(exc.exit_code) == 0
    else:
        raise AssertionError("Expected run command to terminate with exit code 0")

    assert selections
    assert "Multiple 'openclaw' instances found. Select one:" in selections[0][0]
    assert set(selections[0][1]) == {"prindleclaw", "openclaw_2"}
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]]


def test_scan_openclaw_uses_single_registered_openclaw_alias_when_default_alias_missing(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "prindleclaw"
    install_dir.mkdir(parents=True)

    app_meta = AppMetadata(
        name="prindleclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
    )
    registry = MagicMock()
    registry.get_app.side_effect = lambda name: None if str(name).strip() == "openclaw" else app_meta
    registry.list_apps.return_value = [app_meta]

    scanned_targets: list[str] = []
    rendered_reports: list[tuple[dict, str]] = []

    class _FakeScanner:
        def __init__(self, target_dir: str):
            scanned_targets.append(target_dir)

        def scan(self):
            return {"ok": True}

        def render_report(self, result, agent_name: str):
            rendered_reports.append((result, agent_name))

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._fallback_openclaw_default_app", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.scanner.VulnerabilityScanner", _FakeScanner)

    result = runner.invoke(app, ["scan", "openclaw"])

    assert result.exit_code == 0
    assert "Agent 'openclaw' not found" not in result.stdout
    assert scanned_targets == [str(install_dir)]
    assert rendered_reports == [({"ok": True}, "openclaw")]


def test_stop_openclaw_multiple_instances_non_interactive_requires_alias(tmp_path: Path, monkeypatch):
    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = [
        AppMetadata(name="prindleclaw", install_path=str(tmp_path / "installed" / "prindleclaw"), agent_id="openclaw"),
        AppMetadata(name="openclaw_2", install_path=str(tmp_path / "installed" / "openclaw_2"), agent_id="openclaw"),
    ]

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._fallback_openclaw_default_app", lambda *args, **kwargs: None)

    result = runner.invoke(app, ["stop", "openclaw"])

    assert result.exit_code == 1
    assert "Error: Multiple instances found for 'openclaw': prindleclaw, openclaw_2." in result.stdout
    assert "Use an" in result.stdout
    assert "alias." in result.stdout


def test_uninstall_openclaw_not_found_shows_install_guidance(tmp_path: Path, monkeypatch):
    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = []

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)

    result = runner.invoke(app, ["uninstall", "openclaw", "--force"])

    assert result.exit_code == 1
    assert "Error: Agent 'openclaw' not found." in result.stdout
    assert "install openclaw" in result.stdout


def test_uninstall_openclaw_multiple_instances_non_interactive_requires_alias(tmp_path: Path, monkeypatch):
    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = [
        AppMetadata(name="prindleclaw", install_path=str(tmp_path / "installed" / "prindleclaw"), agent_id="openclaw"),
        AppMetadata(name="openclaw_2", install_path=str(tmp_path / "installed" / "openclaw_2"), agent_id="openclaw"),
    ]

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._fallback_openclaw_default_app", lambda *args, **kwargs: None)

    result = runner.invoke(app, ["uninstall", "openclaw", "--force"])

    assert result.exit_code == 1
    assert "Error: Multiple instances found for 'openclaw': prindleclaw, openclaw_2." in result.stdout
    assert "Use an" in result.stdout
    assert "alias." in result.stdout


def test_run_openclaw_status_uses_single_registered_openclaw_alias_when_default_alias_missing(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="prindleclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.side_effect = lambda name: None if str(name).strip() == "openclaw" else app_meta
    registry.list_apps.return_value = [app_meta]

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )
    calls: list[list[str]] = []

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._fallback_openclaw_default_app", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda cmd, **kwargs: calls.append(list(cmd)) or subprocess.CompletedProcess(cmd, 0),
    )

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert "Agent 'openclaw' not found" not in result.stdout
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]]


def test_run_openclaw_status_bypasses_menu_when_shortcut_catalog_lacks_status(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "    ports:\n"
        "      - '18789:18789'\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "    entrypoint:\n"
        "      - node\n"
        "      - dist/index.js\n"
        "    tty: true\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[
            {"label": "Gateway", "service": "openclaw-cli", "command": "gateway"},
            {"label": "TUI", "service": "openclaw-cli", "command": "tui"},
        ],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta
    registry.list_apps.return_value = [app_meta]

    calls: list[list[str]] = []

    def _fake_subprocess_run(cmd, **kwargs):
        cmd_list = list(cmd)
        calls.append(cmd_list)
        if cmd_list[:4] == ["docker", "compose", "ps", "-q"]:
            return subprocess.CompletedProcess(cmd_list, 0, "", "")
        return subprocess.CompletedProcess(cmd_list, 0, "", "")

    def _unexpected_ui_select(*args, **kwargs):
        raise AssertionError("interactive menu should be bypassed for direct command args")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", _unexpected_ui_select)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_subprocess_run)

    result = runner.invoke(app, ["run", "openclaw", "--", "status"])

    assert result.exit_code == 0
    assert ["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"] in calls
    assert "Bypassing interactive menu for direct command args." in result.stdout


def test_openclaw_gateway_exec_override_applies_to_read_only_gateway_probe():
    override = _openclaw_gateway_exec_override("openclaw", "openclaw-cli", ["gateway", "probe"])
    assert override == [
        "docker",
        "compose",
        "exec",
        "-T",
        "openclaw-gateway",
        "node",
        "dist/index.js",
        "gateway",
        "probe",
    ]


def test_openclaw_gateway_exec_override_skips_non_read_only_gateway_subcommands():
    assert _openclaw_gateway_exec_override("openclaw", "openclaw-cli", ["gateway", "approve", "telegram", "123"]) is None
    assert _openclaw_gateway_exec_override("openclaw", "openclaw-cli", ["gateway", "reload"]) is None


def test_openclaw_gateway_background_offer_uses_instance_alias_in_stop_hint(monkeypatch, capsys):
    monkeypatch.setattr("armorer.main._ui_confirm", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.sys.stdin.isatty", lambda: True, raising=False)

    handled = _maybe_offer_openclaw_gateway_background(
        "openclaw",
        {"label": "Gateway", "command": "gateway"},
        ["gateway"],
        0,
        Path("/tmp/openclaw"),
        120,
        instance_alias="prindleclaw",
    )

    out = capsys.readouterr().out
    assert handled is True
    assert "armorer stop prindleclaw" in out
    assert "armorer stop openclaw" not in out


def test_run_openclaw_alias_gateway_background_offer_passes_instance_alias(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="prindleclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway", "service": "openclaw-cli", "command": "gateway"}],
    )
    registry = MagicMock()
    registry.get_app.side_effect = lambda name: app_meta if str(name).strip() == "prindleclaw" else None
    registry.list_apps.return_value = [app_meta]

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway",
                service="openclaw-cli",
                command="gateway",
                category="run",
                description="Run OpenClaw gateway command.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    def _fake_run(cmd, **kwargs):
        args = list(cmd)
        if args[:4] == ["docker", "compose", "run", "--rm"]:
            return subprocess.CompletedProcess(args, 0, "", "")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    seen_offer_aliases: list[str] = []

    def _fake_offer(*args, **kwargs):
        seen_offer_aliases.append(str(kwargs.get("instance_alias") or ""))
        return True

    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", _fake_offer)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw", "gateway"])

    assert result.exit_code == 0
    assert seen_offer_aliases == ["prindleclaw"]


def test_run_openclaw_gateway_approve_skips_gateway_probe_wait_in_direct_shortcut(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "cfg"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway approve",
                service="openclaw-cli",
                command="gateway approve",
                category="run",
                description="Approve a gateway pairing request.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )
    calls: list[list[str]] = []
    probe_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", lambda *args, **kwargs: True)
    monkeypatch.setattr(
        "armorer.main._wait_openclaw_gateway_reachable",
        lambda compose_dir, run_env, timeout_seconds=60: probe_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds))) or True,
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", lambda cmd, **kwargs: calls.append(list(cmd)) or subprocess.CompletedProcess(cmd, 0))

    result = runner.invoke(app, ["run", "openclaw", "gateway", "approve", "telegram", "123"])

    assert result.exit_code == 0
    assert probe_wait_calls == []
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "--no-deps", "openclaw-cli", "gateway", "approve", "telegram", "123"]]


def test_run_openclaw_gateway_approve_skips_gateway_probe_wait_in_menu_shortcut(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway approve", "service": "openclaw-cli", "command": "gateway approve"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    calls: list[list[str]] = []
    probe_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway approve")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", lambda *args, **kwargs: True)
    monkeypatch.setattr(
        "armorer.main._wait_openclaw_gateway_reachable",
        lambda compose_dir, run_env, timeout_seconds=60: probe_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds))) or True,
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", lambda cmd, **kwargs: calls.append(list(cmd)) or subprocess.CompletedProcess(cmd, 0))

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert probe_wait_calls == []
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "--no-deps", "openclaw-cli", "gateway", "approve"]]


def test_run_openclaw_gateway_approve_skips_gateway_probe_wait_in_interactive_shortcuts(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway approve", "service": "openclaw-cli", "command": "gateway approve"}],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    calls: list[list[str]] = []
    probe_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "Gateway approve")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", lambda *args, **kwargs: True)
    monkeypatch.setattr(
        "armorer.main._wait_openclaw_gateway_reachable",
        lambda compose_dir, run_env, timeout_seconds=60: probe_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds))) or True,
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", lambda cmd, **kwargs: calls.append(list(cmd)) or subprocess.CompletedProcess(cmd, 0))

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert probe_wait_calls == []
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "--no-deps", "openclaw-cli", "gateway", "approve"]]


def test_run_openclaw_status_uses_config_dir_registry_override(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None]] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert calls == [
        (["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"], install_dir)
    ]


def test_run_openclaw_status_reports_docker_cli_hint_when_shortcut_execution_raises(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda cmd, **kwargs: (_ for _ in ()).throw(FileNotFoundError(2, "No such file or directory", "docker")),
    )

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 1
    assert "Failed to run command:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout


def test_run_openclaw_status_shortcut_uses_bounded_timeout_and_reports_timeout(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    seen_timeouts: list[int] = []

    def _timed_out_run(cmd, **kwargs):
        timeout_value = int(kwargs.get("timeout", 0) or 0)
        seen_timeouts.append(timeout_value)
        raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout_value)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _timed_out_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 1
    assert seen_timeouts == [120]
    assert "Command timed out after 120s:" in result.stdout


def test_run_openclaw_menu_selected_status_timeout_exits_nonzero(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    seen_timeouts: list[int] = []

    def _timed_out_run(cmd, **kwargs):
        timeout_value = int(kwargs.get("timeout", 0) or 0)
        seen_timeouts.append(timeout_value)
        raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout_value)

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway status")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _timed_out_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 1
    assert seen_timeouts == [120]
    assert "Command timed out after 120s:" in result.stdout


def test_run_openclaw_interactive_shortcut_status_timeout_exits_nonzero(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    seen_timeouts: list[int] = []

    def _timed_out_run(cmd, **kwargs):
        timeout_value = int(kwargs.get("timeout", 0) or 0)
        seen_timeouts.append(timeout_value)
        raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout_value)

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "Gateway status")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _timed_out_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 1
    assert seen_timeouts == [120]
    assert "Command timed out after 120s:" in result.stdout


def test_run_openclaw_direct_shortcut_success_ignores_post_setup_defaults_exception(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._maybe_apply_openclaw_setup_defaults",
        lambda *args, **kwargs: (_ for _ in ()).throw(FileNotFoundError(2, "No such file or directory", "docker")),
    )
    monkeypatch.setattr("armorer.main.subprocess.run", lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0))

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert "Skipping post-setup defaults:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout


def test_run_openclaw_menu_shortcut_success_ignores_post_setup_defaults_exception(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway status")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._maybe_apply_openclaw_setup_defaults",
        lambda *args, **kwargs: (_ for _ in ()).throw(FileNotFoundError(2, "No such file or directory", "docker")),
    )
    monkeypatch.setattr("armorer.main.subprocess.run", lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0))

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert "Skipping post-setup defaults:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout


def test_run_openclaw_interactive_shortcut_success_ignores_post_setup_defaults_exception(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "Gateway status")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._maybe_apply_openclaw_setup_defaults",
        lambda *args, **kwargs: (_ for _ in ()).throw(FileNotFoundError(2, "No such file or directory", "docker")),
    )
    monkeypatch.setattr("armorer.main.subprocess.run", lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0))

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert "Skipping post-setup defaults:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout


def test_run_openclaw_menu_setup_shortcut_uses_bounded_post_setup_timeout_and_keeps_success(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Setup Wizard", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    seen_calls: list[tuple[list[str], int | None]] = []

    def _fake_run(cmd, **kwargs):
        seen_calls.append((list(cmd), kwargs.get("timeout")))
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and cmd[-2:] == ["openclaw-cli", "status"]:
            return subprocess.CompletedProcess(cmd, 0)
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and "config" in cmd and "get" in cmd:
            raise subprocess.TimeoutExpired(cmd=cmd, timeout=kwargs.get("timeout", 0) or 0)
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Setup Wizard")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert "Skipping post-setup defaults:" in result.stdout
    assert seen_calls[0][0][-2:] == ["openclaw-cli", "status"]
    assert seen_calls[1][1] == 60


def test_run_openclaw_menu_setup_shortcut_skips_post_setup_defaults_when_config_get_returns_nonzero(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Setup Wizard", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    seen_calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        seen_calls.append(list(cmd))
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and cmd[-2:] == ["openclaw-cli", "status"]:
            return subprocess.CompletedProcess(cmd, 0)
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and "config" in cmd and "get" in cmd:
            return subprocess.CompletedProcess(
                cmd,
                1,
                "",
                "permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock",
            )
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Setup Wizard")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert "Skipping post-setup defaults:" in result.stdout
    assert "OpenClaw config get failed" in result.stdout
    assert "Docker daemon access is required for this command." in result.stdout
    assert seen_calls[0][-2:] == ["openclaw-cli", "status"]
    assert "config" in seen_calls[1] and "get" in seen_calls[1]


def test_run_openclaw_menu_setup_shortcut_allows_missing_config_path_without_post_setup_warning(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Setup Wizard", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    seen_calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        seen_calls.append(list(cmd))
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and cmd[-2:] == ["openclaw-cli", "status"]:
            return subprocess.CompletedProcess(cmd, 0)
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and "config" in cmd and "get" in cmd:
            return subprocess.CompletedProcess(cmd, 1, "", "Config path not found: channels.telegram.botToken")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Setup Wizard")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert "Skipping post-setup defaults:" not in result.stdout
    assert seen_calls[0][-2:] == ["openclaw-cli", "status"]
    assert "config" in seen_calls[1] and "get" in seen_calls[1]


def test_run_openclaw_menu_setup_shortcut_skips_post_setup_defaults_when_config_set_returns_nonzero(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Setup Wizard", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    seen_calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        seen_calls.append(list(cmd))
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and cmd[-2:] == ["openclaw-cli", "status"]:
            return subprocess.CompletedProcess(cmd, 0)
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and "config" in cmd and "get" in cmd:
            path = cmd[-1]
            values = {
                "channels.telegram.botToken": "123456:runtime_token_value",
                "channels.telegram.groupPolicy": "allowlist",
                "channels.telegram.groupAllowFrom": "[]",
                "channels.telegram.allowFrom": "[]",
            }
            return subprocess.CompletedProcess(cmd, 0, values.get(path, ""), "")
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and "config" in cmd and "set" in cmd:
            return subprocess.CompletedProcess(
                cmd,
                1,
                "",
                "permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock",
            )
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Setup Wizard")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert "Skipping post-setup defaults:" in result.stdout
    assert "Docker daemon access is required for this command." in result.stdout
    assert "config" in seen_calls[1] and "get" in seen_calls[1]
    assert any("config" in call and "set" in call for call in seen_calls)


def test_run_openclaw_menu_setup_shortcut_ignores_warning_only_config_get_stderr(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Setup Wizard", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    seen_calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        seen_calls.append(list(cmd))
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and cmd[-2:] == ["openclaw-cli", "status"]:
            return subprocess.CompletedProcess(cmd, 0)
        if cmd[:4] == ["docker", "compose", "run", "--rm"] and "config" in cmd and "get" in cmd:
            return subprocess.CompletedProcess(
                cmd,
                0,
                "",
                "WARN[0000] Found orphan containers ([legacy-openclaw]) for this project.\n",
            )
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Setup Wizard")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert "Skipping post-setup defaults:" not in result.stdout
    assert seen_calls[0][-2:] == ["openclaw-cli", "status"]
    assert "config" in seen_calls[1] and "get" in seen_calls[1]
    assert not any("config" in call and "set" in call for call in seen_calls)


def test_openclaw_post_setup_pairing_failure_shows_daemon_hint(tmp_path: Path, monkeypatch, capsys):
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True)

    def _fake_run(cmd, **kwargs):
        args = list(cmd)
        if args[:8] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli", "config", "get"]:
            if args[8] == "channels.telegram.botToken":
                return subprocess.CompletedProcess(args, 0, "123456:runtime_token_value", "")
            if args[8] == "channels.telegram.groupPolicy":
                return subprocess.CompletedProcess(args, 0, "open", "")
            return subprocess.CompletedProcess(args, 0, "", "")
        if args[:8] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli", "pairing", "approve"]:
            return subprocess.CompletedProcess(
                args,
                1,
                "",
                "permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock",
            )
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main._ui_text", lambda *args, **kwargs: "PAIR123")
    monkeypatch.setattr("armorer.main.sys.stdin.isatty", lambda: True, raising=False)

    _maybe_apply_openclaw_setup_defaults("openclaw", "Setup Wizard", compose_dir, {})

    out = capsys.readouterr().out
    assert "Telegram pairing approval failed:" in out
    assert "Docker daemon access is required for this command." in out


def test_openclaw_post_setup_pairing_failure_uses_exit_code_when_output_empty(tmp_path: Path, monkeypatch, capsys):
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True)

    def _fake_run(cmd, **kwargs):
        args = list(cmd)
        if args[:8] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli", "config", "get"]:
            if args[8] == "channels.telegram.botToken":
                return subprocess.CompletedProcess(args, 0, "123456:runtime_token_value", "")
            if args[8] == "channels.telegram.groupPolicy":
                return subprocess.CompletedProcess(args, 0, "open", "")
            return subprocess.CompletedProcess(args, 0, "", "")
        if args[:8] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli", "pairing", "approve"]:
            return subprocess.CompletedProcess(args, 7, "", "")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main._ui_text", lambda *args, **kwargs: "PAIR123")
    monkeypatch.setattr("armorer.main.sys.stdin.isatty", lambda: True, raising=False)

    _maybe_apply_openclaw_setup_defaults("openclaw", "Setup Wizard", compose_dir, {})

    out = capsys.readouterr().out
    assert "Telegram pairing approval failed: exit code 7" in out


def test_openclaw_post_setup_pairing_gateway_start_timeout_uses_alias_agnostic_guidance(
    tmp_path: Path, monkeypatch, capsys
):
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True)

    def _fake_run(cmd, **kwargs):
        args = list(cmd)
        if args[:8] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli", "config", "get"]:
            if args[8] == "channels.telegram.botToken":
                return subprocess.CompletedProcess(args, 0, "123456:runtime_token_value", "")
            if args[8] == "channels.telegram.groupPolicy":
                return subprocess.CompletedProcess(args, 0, "open", "")
            return subprocess.CompletedProcess(args, 0, "", "")
        if args[:8] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli", "pairing", "approve"]:
            return subprocess.CompletedProcess(args, 0, "approved", "")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main._ui_text", lambda *args, **kwargs: "PAIR123")
    monkeypatch.setattr("armorer.main._ui_confirm", lambda *args, **kwargs: True)
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.sys.stdin.isatty", lambda: True, raising=False)

    _maybe_apply_openclaw_setup_defaults("openclaw", "Setup Wizard", compose_dir, {})

    out = capsys.readouterr().out
    assert "Gateway did not become ready." in out
    assert "armorer run <alias> --mode background" in out
    assert "armorer run openclaw --mode background" not in out


def test_run_openclaw_background_reports_docker_cli_hint_when_compose_up_raises(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    def _fake_run(cmd, **kwargs):
        if cmd[:3] == ["docker", "compose", "up"]:
            raise FileNotFoundError(2, "No such file or directory", "docker")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "--mode", "background"])

    assert result.exit_code == 1
    assert "docker compose up failed:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout


def test_run_openclaw_background_reports_docker_daemon_hint_when_compose_up_returns_nonzero(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    def _fake_run(cmd, **kwargs):
        if cmd[:3] == ["docker", "compose", "up"]:
            return subprocess.CompletedProcess(
                cmd,
                1,
                stdout="",
                stderr=(
                    "permission denied while trying to connect to the Docker daemon socket at "
                    "unix:///var/run/docker.sock: connect: operation not permitted"
                ),
            )
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._attempt_agentic_run_repair", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "--mode", "background"])

    assert result.exit_code == 1
    assert "docker compose up failed:" in result.stdout
    assert "Docker daemon access is required for this command." in result.stdout


def test_run_openclaw_interactive_custom_status_timeout_exits_nonzero(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    seen_timeouts: list[int] = []
    run_calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        run_calls.append(list(cmd))
        if cmd[:4] == ["docker", "compose", "ps", "-q"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:4] == ["docker", "compose", "run", "--rm"]:
            timeout_value = int(kwargs.get("timeout", 0) or 0)
            seen_timeouts.append(timeout_value)
            raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout_value)
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._pick_interactive_service", lambda *args, **kwargs: "openclaw-cli")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._attempt_agentic_run_repair", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "--mode", "interactive", "status"])

    assert result.exit_code == 1
    assert seen_timeouts == [120]
    assert "Command timed out after 120s:" in result.stdout
    assert run_calls[0] == ["docker", "compose", "ps", "-q"]


def test_run_openclaw_interactive_custom_status_reports_docker_cli_hint_when_run_raises(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    def _fake_run(cmd, **kwargs):
        if cmd[:4] == ["docker", "compose", "ps", "-q"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:4] == ["docker", "compose", "run", "--rm"]:
            raise FileNotFoundError(2, "No such file or directory", "docker")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._pick_interactive_service", lambda *args, **kwargs: "openclaw-cli")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._attempt_agentic_run_repair", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "--mode", "interactive", "status"])

    assert result.exit_code == 1
    assert "Failed to run command:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout


def test_run_openclaw_interactive_custom_status_reports_daemon_hint_when_preflight_ps_returns_nonzero(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    run_calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        run_calls.append(list(cmd))
        if cmd[:4] == ["docker", "compose", "ps", "-q"]:
            return subprocess.CompletedProcess(
                cmd,
                1,
                stdout="",
                stderr=(
                    "permission denied while trying to connect to the Docker daemon socket at "
                    "unix:///var/run/docker.sock: connect: operation not permitted"
                ),
            )
        if cmd[:4] == ["docker", "compose", "run", "--rm"]:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._pick_interactive_service", lambda *args, **kwargs: "openclaw-cli")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._attempt_agentic_run_repair", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "--mode", "interactive", "status"])

    assert result.exit_code == 1
    assert "Failed to inspect running services:" in result.stdout
    assert "Docker daemon access is required for this command." in result.stdout
    assert run_calls[0] == ["docker", "compose", "ps", "-q"]


def test_run_openclaw_interactive_custom_status_reports_docker_cli_hint_when_preflight_ps_raises(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    def _fake_run(cmd, **kwargs):
        if cmd[:4] == ["docker", "compose", "ps", "-q"]:
            raise FileNotFoundError(2, "No such file or directory", "docker")
        if cmd[:4] == ["docker", "compose", "run", "--rm"]:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._pick_interactive_service", lambda *args, **kwargs: "openclaw-cli")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._attempt_agentic_run_repair", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "--mode", "interactive", "status"])

    assert result.exit_code == 1
    assert "Failed to inspect running services:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout


def test_run_openclaw_interactive_custom_status_reports_daemon_hint_when_preflight_down_returns_nonzero(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    run_calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        run_calls.append(list(cmd))
        if cmd[:4] == ["docker", "compose", "ps", "-q"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="container-id\n", stderr="")
        if cmd[:3] == ["docker", "compose", "down"]:
            return subprocess.CompletedProcess(
                cmd,
                1,
                stdout="",
                stderr=(
                    "permission denied while trying to connect to the Docker daemon socket at "
                    "unix:///var/run/docker.sock: connect: operation not permitted"
                ),
            )
        if cmd[:4] == ["docker", "compose", "run", "--rm"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._pick_interactive_service", lambda *args, **kwargs: "openclaw-cli")
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._attempt_agentic_run_repair", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "--mode", "interactive", "status"])

    assert result.exit_code == 0
    assert "Stopping background services to free ports..." in result.stdout
    assert "Failed to stop background services:" in result.stdout
    assert "Docker daemon access is required for this command." in result.stdout
    assert run_calls[0] == ["docker", "compose", "ps", "-q"]
    assert run_calls[1] == ["docker", "compose", "down"]


def test_run_openclaw_menu_stop_reports_docker_cli_hint_when_compose_down_raises(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
        run_preference=None,
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    def _fake_run(cmd, **kwargs):
        if cmd[:3] == ["docker", "compose", "down"]:
            raise FileNotFoundError(2, "No such file or directory", "docker")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "action::stop")
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 1
    assert "Failed to stop services:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout


def test_run_openclaw_menu_stop_reports_daemon_hint_when_compose_down_returns_nonzero(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
        run_preference=None,
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    def _fake_run(cmd, **kwargs):
        if cmd[:3] == ["docker", "compose", "down"]:
            return subprocess.CompletedProcess(
                cmd,
                1,
                stdout="",
                stderr=(
                    "permission denied while trying to connect to the Docker daemon socket at "
                    "unix:///var/run/docker.sock: connect: operation not permitted"
                ),
            )
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "action::stop")
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 1
    assert "Failed to stop services:" in result.stdout
    assert "Docker daemon access is required for this command." in result.stdout


def test_run_openclaw_status_uses_gateway_exec_after_starting_prereqs(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        return True

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("status read-only path should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "exec", "-T", "openclaw-gateway", "node", "dist/index.js", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "Using gateway exec path for OpenClaw" in result.stdout


def test_run_openclaw_alias_status_uses_gateway_exec_after_starting_prereqs(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "prindleclaw": {
                    "name": "prindleclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        return True

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("status read-only path should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw", "status"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "exec", "-T", "openclaw-gateway", "node", "dist/index.js", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "Using gateway exec path for OpenClaw" in result.stdout


def test_run_openclaw_alias_status_aborts_when_gateway_probe_reports_fatal_docker_error(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "prindleclaw": {
                    "name": "prindleclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = True
        return False

    _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("status read-only path should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw", "status"])

    assert result.exit_code == 1
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == []
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway probe hit a fatal Docker error; aborting run instead of" in result.stdout
    assert "fallback execution." in result.stdout
    assert "Falling back to compose run with dependencies" not in result.stdout


def test_run_openclaw_alias_status_falls_back_to_compose_run_when_gateway_probe_is_unreachable(
    tmp_path: Path, monkeypatch
):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "prindleclaw": {
                    "name": "prindleclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False
        return False

    _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("status read-only path should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw", "status"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway did not report reachable in time; continuing anyway." in result.stdout
    assert "Falling back to compose run with dependencies" in result.stdout
    assert "gateway probe was" in result.stdout
    assert "not reachable." in result.stdout
    assert "Using gateway exec path for OpenClaw" not in result.stdout


def test_run_openclaw_status_falls_back_to_compose_run_when_gateway_probe_is_unreachable(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        return False

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("status read-only path should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway did not report reachable in time; continuing anyway." in result.stdout
    assert "Falling back to compose run with dependencies" in result.stdout
    assert "gateway probe was" in result.stdout
    assert "not reachable." in result.stdout
    assert "Using gateway exec path for OpenClaw" not in result.stdout


def test_run_openclaw_status_aborts_when_gateway_probe_reports_fatal_docker_error(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = True
        return False

    _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(bool(persist)),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("status read-only path should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 1
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == []
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway probe hit a fatal Docker error; aborting run instead of" in result.stdout
    assert "fallback execution." in result.stdout
    assert "Falling back to compose run with dependencies" not in result.stdout
    assert "OpenClaw gateway did not report reachable in time; continuing anyway." not in result.stdout


def test_run_openclaw_status_prefers_armorer_config_dir_registry_override(tmp_path: Path, monkeypatch):
    armorer_config_dir = tmp_path / "armorer-env"
    legacy_config_dir = tmp_path / "legacy-env"
    armorer_config_dir.mkdir(parents=True)
    legacy_config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (armorer_config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )
    (legacy_config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(tmp_path / "wrong" / "openclaw"),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None]] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("ARMORER_CONFIG_DIR", str(armorer_config_dir))
    monkeypatch.setenv("CONFIG_DIR", str(legacy_config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert calls == [
        (["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"], install_dir)
    ]


def test_run_openclaw_status_falls_back_to_default_install_root_when_registry_missing(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = []
    registry.add_app.side_effect = PermissionError("read-only registry")

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert "not found" not in result.stdout.lower()
    assert registry.add_app.called
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]]


def test_list_openclaw_falls_back_to_default_install_root_when_registry_missing(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = []
    registry.add_app.side_effect = PermissionError("read-only registry")

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    def _fake_run(cmd, **kwargs):
        if list(cmd[:2]) == ["docker", "ps"]:
            return subprocess.CompletedProcess(cmd, 0, "", "")
        if list(cmd[:3]) == ["docker", "compose", "ps"]:
            return subprocess.CompletedProcess(cmd, 0, "[]", "")
        raise AssertionError(f"unexpected subprocess call: {cmd}")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["list"])

    assert result.exit_code == 0
    assert "No agents installed yet" not in result.stdout
    assert "openclaw" in result.stdout
    assert registry.add_app.called
    assert registry.add_app.call_args[0][0].install_path == str(install_dir)


def test_run_openclaw_status_falls_back_when_registry_install_path_is_stale(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    store = {
        "openclaw": AppMetadata(
            name="openclaw",
            install_path=str(tmp_path / "missing" / "openclaw"),
            agent_id="openclaw",
            run_commands=[],
        )
    }

    class _FakeRegistry:
        def get_app(self, name):
            return store.get(name)

        def list_apps(self):
            return list(store.values())

        def add_app(self, app):
            store[app.name] = app

    registry = _FakeRegistry()

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert store["openclaw"].install_path == str(install_dir)
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]]


def test_list_openclaw_replaces_stale_registry_path_with_default_install_root(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    stale_dir = tmp_path / "stale" / "openclaw"
    stale_dir.mkdir(parents=True)

    store = {
        "openclaw": AppMetadata(
            name="openclaw",
            install_path=str(stale_dir),
            agent_id="openclaw",
            run_commands=[],
        )
    }

    class _FakeRegistry:
        def get_app(self, name):
            return store.get(name)

        def list_apps(self):
            return list(store.values())

        def add_app(self, app):
            store[app.name] = app

    def _fake_run(cmd, **kwargs):
        if list(cmd[:2]) == ["docker", "ps"]:
            return subprocess.CompletedProcess(cmd, 0, "", "")
        if list(cmd[:3]) == ["docker", "compose", "ps"]:
            return subprocess.CompletedProcess(cmd, 0, "[]", "")
        raise AssertionError(f"unexpected subprocess call: {cmd}")

    registry = _FakeRegistry()
    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["list"])

    assert result.exit_code == 0
    assert "openclaw" in result.stdout
    assert store["openclaw"].install_path == str(install_dir)


def test_run_openclaw_status_falls_back_when_registry_install_path_exists_but_is_not_runtime(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    stale_dir = tmp_path / "stale" / "openclaw"
    stale_dir.mkdir(parents=True)

    store = {
        "openclaw": AppMetadata(
            name="openclaw",
            install_path=str(stale_dir),
            agent_id="openclaw",
            run_commands=[],
        )
    }

    class _FakeRegistry:
        def get_app(self, name):
            return store.get(name)

        def list_apps(self):
            return list(store.values())

        def add_app(self, app):
            store[app.name] = app

    registry = _FakeRegistry()

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert store["openclaw"].install_path == str(install_dir)
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]]


def test_install_openclaw_reinstalls_when_default_alias_points_to_missing_runtime(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    store = {
        "openclaw": AppMetadata(
            name="openclaw",
            install_path=str(tmp_path / "missing" / "openclaw"),
            agent_id="openclaw",
        )
    }
    installer_calls: list[dict[str, object]] = []

    class _FakeRegistry:
        def get_app(self, name):
            return store.get(name)

        def list_apps(self):
            return list(store.values())

        def add_app(self, app):
            store[app.name] = app

    class _FakeInstaller:
        def __init__(self, app_name, profile, mount_paths, options):
            installer_calls.append(
                {
                    "app_name": app_name,
                    "mount_paths": list(mount_paths),
                    "instance_name": options.get("instance_name"),
                    "run_after_install": options.get("run_after_install"),
                }
            )
            self.instance_name = str(options.get("instance_name") or app_name)
            self.install_dir = install_dir
            self._docker_compose_cwd = install_dir
            self._did_autostart_after_install = True

        def install(self):
            install_dir.mkdir(parents=True, exist_ok=True)
            (install_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )
            store[self.instance_name] = AppMetadata(
                name=self.instance_name,
                install_path=str(install_dir),
                agent_id="openclaw",
            )

    registry = _FakeRegistry()

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._ensure_otel_bridge_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._ensure_telegram_gateway_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._is_profile_only_compose", lambda install_dir: False)
    monkeypatch.setattr("armorer.installers.dynamic.DynamicInstaller", _FakeInstaller)

    result = runner.invoke(app, ["install", "openclaw", "--yes", "--skip-scan", "--alias", "openclaw"])

    assert result.exit_code == 0
    assert installer_calls == [
        {
            "app_name": "openclaw",
            "mount_paths": [str(tmp_path)],
            "instance_name": "openclaw",
            "run_after_install": True,
        }
    ]
    assert store["openclaw"].install_path == str(install_dir)


def test_install_openclaw_uses_new_alias_when_default_runtime_exists_but_registry_is_missing(tmp_path: Path, monkeypatch):
    existing_install_dir = tmp_path / "installed" / "openclaw"
    existing_install_dir.mkdir(parents=True)
    (existing_install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    new_install_dir = tmp_path / "installed" / "openclaw_2"
    installer_calls: list[dict[str, object]] = []

    class _FakeRegistry:
        def get_app(self, name):
            return None

        def list_apps(self):
            return []

        def add_app(self, app):
            raise PermissionError("read-only registry")

    class _FakeInstaller:
        def __init__(self, app_name, profile, mount_paths, options):
            installer_calls.append(
                {
                    "app_name": app_name,
                    "mount_paths": list(mount_paths),
                    "instance_name": options.get("instance_name"),
                    "run_after_install": options.get("run_after_install"),
                }
            )
            self.instance_name = str(options.get("instance_name") or app_name)
            self.install_dir = new_install_dir
            self._docker_compose_cwd = new_install_dir
            self._did_autostart_after_install = True

        def install(self):
            new_install_dir.mkdir(parents=True, exist_ok=True)
            (new_install_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", _FakeRegistry)
    monkeypatch.setattr("armorer.main._ensure_otel_bridge_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._ensure_telegram_gateway_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._is_profile_only_compose", lambda install_dir: False)
    monkeypatch.setattr("armorer.installers.dynamic.DynamicInstaller", _FakeInstaller)

    result = runner.invoke(app, ["install", "openclaw", "--yes", "--skip-scan"])

    assert result.exit_code == 0
    assert installer_calls == [
        {
            "app_name": "openclaw",
            "mount_paths": [str(tmp_path)],
            "instance_name": "openclaw_2",
            "run_after_install": True,
        }
    ]


def test_install_openclaw_alias_openclaw_rejects_live_fallback_when_registry_is_missing(tmp_path: Path, monkeypatch):
    existing_install_dir = tmp_path / "installed" / "openclaw"
    existing_install_dir.mkdir(parents=True)
    (existing_install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    installer_calls: list[dict[str, object]] = []

    class _FakeRegistry:
        def get_app(self, name):
            return None

        def list_apps(self):
            return []

        def add_app(self, app):
            raise PermissionError("read-only registry")

    class _FakeInstaller:
        def __init__(self, app_name, profile, mount_paths, options):
            installer_calls.append(
                {
                    "app_name": app_name,
                    "mount_paths": list(mount_paths),
                    "instance_name": options.get("instance_name"),
                }
            )

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", _FakeRegistry)
    monkeypatch.setattr("armorer.main._ensure_otel_bridge_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._ensure_telegram_gateway_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.installers.dynamic.DynamicInstaller", _FakeInstaller)

    result = runner.invoke(app, ["install", "openclaw", "--yes", "--skip-scan", "--alias", "openclaw"])

    assert result.exit_code == 1
    assert "Alias 'openclaw' already exists." in result.stdout
    assert installer_calls == []


def test_install_openclaw_use_openclaw_uses_live_fallback_when_registry_is_missing(tmp_path: Path, monkeypatch):
    existing_install_dir = tmp_path / "installed" / "openclaw"
    existing_install_dir.mkdir(parents=True)
    (existing_install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    seen: list[tuple[str, str | None]] = []

    class _FakeRegistry:
        def get_app(self, name):
            return None

        def list_apps(self):
            return []

        def add_app(self, app):
            raise PermissionError("read-only registry")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", _FakeRegistry)
    monkeypatch.setattr("armorer.main._ensure_otel_bridge_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._ensure_telegram_gateway_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main.run", lambda ctx, agent, mode=None: seen.append((agent, mode)))

    result = runner.invoke(app, ["install", "openclaw", "--yes", "--skip-scan", "--use", "openclaw"])

    assert result.exit_code == 0
    assert seen == [("openclaw", "background")]


def test_install_openclaw_use_missing_alias_shows_install_guidance(tmp_path: Path, monkeypatch):
    class _FakeRegistry:
        def get_app(self, name):
            return None

        def list_apps(self):
            return []

        def add_app(self, app):
            raise PermissionError("read-only registry")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", _FakeRegistry)
    monkeypatch.setattr("armorer.main._ensure_otel_bridge_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._ensure_telegram_gateway_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)

    result = runner.invoke(app, ["install", "openclaw", "--yes", "--skip-scan", "--use", "openclaw_missing"])

    assert result.exit_code == 1
    assert "Alias 'openclaw_missing' not found." in result.stdout
    assert "install openclaw --yes" in result.stdout


def test_install_openclaw_use_missing_alias_lists_installed_aliases(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    install_dir2 = tmp_path / "installed" / "openclaw_2"
    install_dir2.mkdir(parents=True)
    (install_dir2 / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    class _FakeRegistry:
        def get_app(self, name):
            return None

        def list_apps(self):
            return [
                AppMetadata(name="openclaw", install_path=str(install_dir), agent_id="openclaw"),
                AppMetadata(name="openclaw_2", install_path=str(install_dir2), agent_id="openclaw"),
            ]

        def add_app(self, app):
            raise PermissionError("read-only registry")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", _FakeRegistry)
    monkeypatch.setattr("armorer.main._ensure_otel_bridge_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._ensure_telegram_gateway_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)

    result = runner.invoke(app, ["install", "openclaw", "--yes", "--skip-scan", "--use", "openclaw_missing"])

    assert result.exit_code == 1
    assert "Alias 'openclaw_missing' not found." in result.stdout
    assert "Installed OpenClaw aliases: openclaw, openclaw_2." in result.stdout


def test_install_openclaw_offers_initial_setup_when_registry_save_failed_but_runtime_exists(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    prompts: list[str] = []

    class _FakeRegistry:
        def get_app(self, name):
            return None

        def list_apps(self):
            return []

        def add_app(self, app):
            raise PermissionError("read-only registry")

    class _FakeInstaller:
        def __init__(self, app_name, profile, mount_paths, options):
            self.instance_name = str(options.get("instance_name") or app_name)
            self.install_dir = install_dir
            self._docker_compose_cwd = install_dir
            self._did_autostart_after_install = True

        def install(self):
            install_dir.mkdir(parents=True, exist_ok=True)
            (install_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Onboard",
                service="openclaw-cli",
                command="onboard",
                category="setup",
                description="Complete initial setup.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", _FakeRegistry)
    monkeypatch.setattr("armorer.main._ensure_otel_bridge_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._ensure_telegram_gateway_autostart", lambda: None)
    monkeypatch.setattr("armorer.main._check_notifications", lambda: None)
    monkeypatch.setattr("armorer.main._print_logo", lambda: None)
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_confirm", lambda message, default=True: prompts.append(message) or False)
    monkeypatch.setattr("armorer.installers.dynamic.DynamicInstaller", _FakeInstaller)
    monkeypatch.setattr("armorer.main.run", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("run should not be called")))

    result = runner.invoke(app, ["install", "openclaw", "--skip-scan"])

    assert result.exit_code == 0
    assert prompts[:2] == [
        "Would you like to run initial setup for openclaw now?",
        "Would you like to launch openclaw now?",
    ]


def test_stop_openclaw_uses_default_runtime_fallback_when_registry_is_missing(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = []
    registry.add_app.side_effect = PermissionError("read-only registry")

    calls: list[tuple[list[str], Path | None]] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["stop", "openclaw"])

    assert result.exit_code == 0
    assert calls == [(["docker", "compose", "stop"], install_dir)]


def test_stop_openclaw_uses_live_fallback_when_registry_path_is_stale(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    store = {
        "openclaw": AppMetadata(
            name="openclaw",
            install_path=str(tmp_path / "missing" / "openclaw"),
            agent_id="openclaw",
            run_commands=[],
        )
    }

    class _FakeRegistry:
        def get_app(self, name):
            return store.get(name)

        def list_apps(self):
            return list(store.values())

        def add_app(self, app):
            store[app.name] = app

    calls: list[tuple[list[str], Path | None]] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", _FakeRegistry)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["stop", "openclaw"])

    assert result.exit_code == 0
    assert store["openclaw"].install_path == str(install_dir)
    assert calls == [(["docker", "compose", "stop"], install_dir)]


def test_stop_openclaw_reports_docker_cli_hint_when_compose_stop_raises(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    registry = MagicMock()
    registry.get_app.return_value = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
    )

    def _fake_run(cmd, **kwargs):
        if cmd[:3] == ["docker", "compose", "stop"]:
            raise FileNotFoundError(2, "No such file or directory", "docker")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["stop", "openclaw"])

    assert result.exit_code == 1
    assert "Failed to stop services:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout


def test_stop_openclaw_reports_daemon_hint_when_compose_stop_returns_nonzero(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    registry = MagicMock()
    registry.get_app.return_value = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
    )

    def _fake_run(cmd, **kwargs):
        if cmd[:3] == ["docker", "compose", "stop"]:
            return subprocess.CompletedProcess(
                cmd,
                1,
                stdout="",
                stderr=(
                    "permission denied while trying to connect to the Docker daemon socket at "
                    "unix:///var/run/docker.sock: connect: operation not permitted"
                ),
            )
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["stop", "openclaw"])

    assert result.exit_code == 1
    assert "Failed to stop services:" in result.stdout
    assert "Docker daemon access is required for this command." in result.stdout


def test_uninstall_openclaw_uses_default_runtime_fallback_when_registry_is_missing(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = []
    registry.add_app.side_effect = PermissionError("read-only registry")

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None]] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd")))
        if list(cmd[:3]) == ["docker", "ps", "-a"]:
            return subprocess.CompletedProcess(cmd, 0, "", "")
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["uninstall", "openclaw", "--force"])

    assert result.exit_code == 0
    assert calls[0] == (["docker", "compose", "down", "-v"], install_dir)
    assert calls[1][0][:3] == ["docker", "ps", "-a"]
    registry.remove_app.assert_called_once_with("openclaw")
    assert not install_dir.exists()


def test_uninstall_openclaw_uses_live_fallback_when_registry_path_is_stale(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    store = {
        "openclaw": AppMetadata(
            name="openclaw",
            install_path=str(tmp_path / "missing" / "openclaw"),
            agent_id="openclaw",
            run_commands=[],
        )
    }

    class _FakeRegistry:
        def get_app(self, name):
            return store.get(name)

        def list_apps(self):
            return list(store.values())

        def add_app(self, app):
            store[app.name] = app

        def remove_app(self, name):
            store.pop(name, None)

    registry = _FakeRegistry()
    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    calls: list[tuple[list[str], Path | None]] = []

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd")))
        if list(cmd[:3]) == ["docker", "ps", "-a"]:
            return subprocess.CompletedProcess(cmd, 0, "", "")
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["uninstall", "openclaw", "--force"])

    assert result.exit_code == 0
    assert calls[0] == (["docker", "compose", "down", "-v"], install_dir)
    assert calls[1][0][:3] == ["docker", "ps", "-a"]
    assert store == {}
    assert not install_dir.exists()


def test_uninstall_openclaw_reports_docker_cli_hint_when_compose_down_raises_without_force(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    def _fake_run(cmd, **kwargs):
        if list(cmd[:4]) == ["docker", "compose", "down", "-v"]:
            raise FileNotFoundError(2, "No such file or directory", "docker")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.questionary.confirm", lambda *args, **kwargs: SimpleNamespace(ask=lambda: True))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["uninstall", "openclaw"])

    assert result.exit_code == 1
    assert "docker compose down failed:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout
    assert "Aborting uninstall to prevent orphaned containers." in result.stdout
    registry.remove_app.assert_not_called()


def test_uninstall_openclaw_force_continues_when_compose_down_raises_with_docker_cli_hint(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        if list(cmd[:4]) == ["docker", "compose", "down", "-v"]:
            raise FileNotFoundError(2, "No such file or directory", "docker")
        if list(cmd[:3]) == ["docker", "ps", "-a"]:
            return subprocess.CompletedProcess(cmd, 0, "", "")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["uninstall", "openclaw", "--force"])

    assert result.exit_code == 0
    assert "docker compose down failed:" in result.stdout
    assert "Docker CLI is required for this command." in result.stdout
    assert "⚠ Forcing uninstall despite errors..." in result.stdout
    assert calls[0] == ["docker", "compose", "down", "-v"]
    assert calls[1][:3] == ["docker", "ps", "-a"]
    registry.remove_app.assert_called_once_with("openclaw")
    assert not install_dir.exists()


def test_scan_openclaw_uses_default_runtime_fallback_when_registry_is_missing(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    registry = MagicMock()
    registry.get_app.return_value = None
    registry.list_apps.return_value = []
    registry.add_app.side_effect = PermissionError("read-only registry")
    seen: dict[str, object] = {}

    class _FakeScanner:
        def __init__(self, target_dir):
            seen["target_dir"] = target_dir

        def scan(self):
            seen["scanned"] = True
            return {"ok": True}

        def render_report(self, result, agent):
            seen["render"] = (result, agent)

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.scanner.VulnerabilityScanner", _FakeScanner)

    result = runner.invoke(app, ["scan", "openclaw"])

    assert result.exit_code == 0
    assert seen["target_dir"] == str(install_dir)
    assert seen["scanned"] is True
    assert seen["render"] == ({"ok": True}, "openclaw")


def test_run_openclaw_status_continues_when_run_command_refresh_cannot_save(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta
    registry.add_app.side_effect = PermissionError("read-only registry")

    known_spec = SimpleNamespace(
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert "Skipping registry metadata update during run command refresh" not in result.stdout
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]]


def test_run_openclaw_applies_runtime_defaults_before_local_image_probe(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ${OPENCLAW_IMAGE:-openclaw:local}\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Onboard", "service": "openclaw-cli", "command": "onboard"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    order: list[str] = []
    seen_runtime_env: dict[str, str] = {}

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    def _fake_defaults(compose_dir, run_env, persist=True):
        order.append("defaults")
        run_env["OPENCLAW_IMAGE"] = "ghcr.io/openclaw/openclaw:main"

    def _fake_local_images(install_dir, runtime_env=None):
        order.append("local_images")
        if isinstance(runtime_env, dict):
            seen_runtime_env.update(runtime_env)

    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", _fake_defaults)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", _fake_local_images)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0),
    )

    result = runner.invoke(app, ["run", "openclaw", "onboard"])

    assert result.exit_code == 0
    assert order[:2] == ["defaults", "local_images"]
    assert seen_runtime_env["OPENCLAW_IMAGE"] == "ghcr.io/openclaw/openclaw:main"


def test_run_openclaw_status_skips_gateway_token_reconcile(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("status should skip reconciliation")),
    )
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0),
    )

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0


def test_run_openclaw_status_uses_non_persistent_runtime_default_helpers(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0),
    )

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]


def test_run_openclaw_status_reuses_prepared_runtime_env_for_execution(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ${OPENCLAW_IMAGE:-openclaw:local}\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )
    seen_envs: list[dict[str, str]] = []

    def _fake_defaults(compose_dir, run_env, persist=True):
        run_env["OPENCLAW_IMAGE"] = "ghcr.io/openclaw/openclaw:main"
        run_env["OPENCLAW_GATEWAY_TOKEN"] = "1234567890abcdef1234567890abcdef"

    def _fake_run(cmd, **kwargs):
        seen_envs.append(dict(kwargs.get("env") or {}))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", _fake_defaults)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("status should skip reconciliation")),
    )
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert seen_envs
    assert seen_envs[0]["OPENCLAW_IMAGE"] == "ghcr.io/openclaw/openclaw:main"
    assert seen_envs[0]["OPENCLAW_GATEWAY_TOKEN"] == "1234567890abcdef1234567890abcdef"


def test_run_openclaw_status_skips_repo_update_check(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("status should skip repo update checks")),
    )
    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0),
    )

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 0
    assert "Checking for agent updates" not in result.stdout


def test_run_openclaw_gateway_status_prefers_specific_read_only_shortcut(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[
            {"label": "Gateway", "service": "openclaw-gateway", "command": ""},
            {"label": "Gateway status", "service": "openclaw-cli", "command": "status"},
        ],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[list[str]] = []

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("gateway status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("gateway status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("gateway status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("gateway status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("gateway status should skip repo update checks")),
    )

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw", "gateway", "status"])

    assert result.exit_code == 0
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "Checking for agent updates" not in result.stdout


def test_run_openclaw_menu_selected_status_uses_read_only_runtime_path(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=[],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[list[str]] = []

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("menu status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("menu status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("menu status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._should_ensure_prereq_services", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip repo update checks")),
    )

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert calls == [["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "Checking for agent updates" not in result.stdout


def test_run_openclaw_menu_selected_status_uses_gateway_exec_after_starting_prereqs(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        return True

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("menu status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("menu status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("menu status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "exec", "-T", "openclaw-gateway", "node", "dist/index.js", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "Using gateway exec path for OpenClaw" in result.stdout


def test_run_openclaw_alias_menu_selected_status_uses_gateway_exec_after_starting_prereqs(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="prindleclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.side_effect = lambda name: app_meta if str(name).strip() == "prindleclaw" else None

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        return True

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("menu status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("menu status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("menu status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "exec", "-T", "openclaw-gateway", "node", "dist/index.js", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "Using gateway exec path for OpenClaw" in result.stdout
    assert "Agent 'prindleclaw' not found" not in result.stdout


def test_run_openclaw_menu_selected_status_falls_back_to_compose_run_when_gateway_probe_is_unreachable(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        return False

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("menu status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("menu status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("menu status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway did not report reachable in time; continuing anyway." in result.stdout
    assert "Falling back to compose run with dependencies" in result.stdout
    assert "gateway probe was" in result.stdout
    assert "not reachable." in result.stdout
    assert "Using gateway exec path for OpenClaw" not in result.stdout


def test_run_openclaw_menu_selected_status_aborts_on_fatal_gateway_probe_error(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = True
        return False

    _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("menu status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("menu status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("menu status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 1
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == []
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway probe hit a fatal Docker error; aborting run instead of" in result.stdout
    assert "fallback execution." in result.stdout
    assert "Falling back to compose run with dependencies" not in result.stdout


def test_run_openclaw_alias_menu_selected_status_aborts_on_fatal_gateway_probe_error(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="prindleclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.side_effect = lambda name: app_meta if str(name).strip() == "prindleclaw" else None

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = True
        return False

    _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("menu status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("menu status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("menu status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw"])

    assert result.exit_code == 1
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == []
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway probe hit a fatal Docker error; aborting run instead of" in result.stdout
    assert "fallback execution." in result.stdout
    assert "Falling back to compose run with dependencies" not in result.stdout
    assert "Agent 'prindleclaw' not found" not in result.stdout


def test_run_openclaw_alias_menu_selected_status_falls_back_to_compose_run_when_gateway_probe_is_unreachable(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="prindleclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
    )
    registry = MagicMock()
    registry.get_app.side_effect = lambda name: app_meta if str(name).strip() == "prindleclaw" else None

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False
        return False

    _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "run::Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("menu status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("menu status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("menu status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("menu status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway did not report reachable in time; continuing anyway." in result.stdout
    assert "Falling back to compose run with dependencies" in result.stdout
    assert "gateway probe was" in result.stdout
    assert "not reachable." in result.stdout
    assert "Using gateway exec path for OpenClaw" not in result.stdout
    assert "Agent 'prindleclaw' not found" not in result.stdout


def test_run_openclaw_interactive_shortcut_status_uses_gateway_exec_after_starting_prereqs(tmp_path: Path, monkeypatch):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        return True

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("interactive status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("interactive status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("interactive status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "exec", "-T", "openclaw-gateway", "node", "dist/index.js", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "Using gateway exec path for OpenClaw" in result.stdout


def test_run_openclaw_alias_interactive_shortcut_status_uses_gateway_exec_after_starting_prereqs(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="prindleclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.side_effect = lambda name: app_meta if str(name).strip() == "prindleclaw" else None

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        return True

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("interactive status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("interactive status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("interactive status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "exec", "-T", "openclaw-gateway", "node", "dist/index.js", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "Using gateway exec path for OpenClaw" in result.stdout
    assert "Agent 'prindleclaw' not found" not in result.stdout


def test_run_openclaw_interactive_shortcut_status_falls_back_to_compose_run_when_gateway_probe_is_unreachable(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        return False

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("interactive status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("interactive status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("interactive status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway did not report reachable in time; continuing anyway." in result.stdout
    assert "Falling back to compose run with dependencies" in result.stdout
    assert "gateway probe was" in result.stdout
    assert "not reachable." in result.stdout
    assert "Using gateway exec path for OpenClaw" not in result.stdout


def test_run_openclaw_alias_interactive_shortcut_status_falls_back_to_compose_run_when_gateway_probe_is_unreachable(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="prindleclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.side_effect = lambda name: app_meta if str(name).strip() == "prindleclaw" else None

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False
        return False

    _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("interactive status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("interactive status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("interactive status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw"])

    assert result.exit_code == 0
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == [
        (
            ["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"],
            install_dir,
            {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"},
        )
    ]
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway did not report reachable in time; continuing anyway." in result.stdout
    assert "Falling back to compose run with dependencies" in result.stdout
    assert "gateway probe was" in result.stdout
    assert "not reachable." in result.stdout
    assert "Using gateway exec path for OpenClaw" not in result.stdout
    assert "Agent 'prindleclaw' not found" not in result.stdout


def test_run_openclaw_interactive_shortcut_status_aborts_on_fatal_gateway_probe_error(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="openclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.return_value = app_meta

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = True
        return False

    _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("interactive status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("interactive status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("interactive status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "openclaw"])

    assert result.exit_code == 1
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == []
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway probe hit a fatal Docker error; aborting run instead of" in result.stdout
    assert "fallback execution." in result.stdout
    assert "Falling back to compose run with dependencies" not in result.stdout


def test_run_openclaw_alias_interactive_shortcut_status_aborts_on_fatal_gateway_probe_error(
    tmp_path: Path, monkeypatch
):
    install_dir = tmp_path / "installed" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / ".git").mkdir()
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    app_meta = AppMetadata(
        name="prindleclaw",
        install_path=str(install_dir),
        agent_id="openclaw",
        run_commands=[{"label": "Gateway status", "service": "openclaw-cli", "command": "status"}],
        run_preference="interactive",
    )
    registry = MagicMock()
    registry.get_app.side_effect = lambda name: app_meta if str(name).strip() == "prindleclaw" else None

    known_spec = SimpleNamespace(
        run_commands=[],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
    )
    runtime_persist_flags: list[bool] = []
    origin_persist_flags: list[bool] = []
    calls: list[tuple[list[str], Path | None, dict | None]] = []
    prereq_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    gateway_wait_calls: list[tuple[Path, dict[str, str], int]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        prereq_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return True

    def _fake_wait_openclaw_gateway_reachable(
        compose_dir: Path,
        run_env: dict[str, str],
        timeout_seconds: int = 60,
    ) -> bool:
        gateway_wait_calls.append((compose_dir, dict(run_env), int(timeout_seconds)))
        _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = True
        return False

    _fake_wait_openclaw_gateway_reachable.last_failure_is_fatal = False

    monkeypatch.setattr("armorer.main.AppRegistry", lambda: registry)
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_armorer_core", lambda: None)
    monkeypatch.setattr("armorer.main._apply_learned_repairs_before_run", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ui_select", lambda *args, **kwargs: "Gateway status")
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_path_env_defaults",
        lambda install_dir: (_ for _ in ()).throw(AssertionError("interactive status should skip path env persistence")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_local_images",
        lambda install_dir, runtime_env=None: (_ for _ in ()).throw(AssertionError("interactive status should skip local image checks")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_runtime_defaults",
        lambda compose_dir, run_env, persist=True: runtime_persist_flags.append(persist),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_openclaw_gateway_allowed_origins",
        lambda compose_dir, persist=True: origin_persist_flags.append(persist),
    )
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    monkeypatch.setattr(
        "armorer.main._reconcile_openclaw_gateway_token",
        lambda compose_dir, run_env: (_ for _ in ()).throw(AssertionError("interactive status should skip reconciliation")),
    )
    monkeypatch.setattr(
        "armorer.main._ensure_runtime_security_approvals",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip approval checks")),
    )
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._maybe_offer_openclaw_gateway_background", lambda *args, **kwargs: False)
    monkeypatch.setattr("armorer.main._maybe_apply_openclaw_setup_defaults", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._recent_install_skip_update", lambda *args, **kwargs: False)
    monkeypatch.setattr(
        "armorer.main._is_repo_update_available",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("interactive status should skip repo update checks")),
    )
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main._wait_openclaw_gateway_reachable", _fake_wait_openclaw_gateway_reachable)

    def _fake_run(cmd, **kwargs):
        calls.append((list(cmd), kwargs.get("cwd"), kwargs.get("env")))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)

    result = runner.invoke(app, ["run", "prindleclaw"])

    assert result.exit_code == 1
    assert prereq_calls == [
        (install_dir, ["openclaw-gateway"], 120, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})
    ]
    assert gateway_wait_calls == [
        (install_dir, {"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"}, 120)
    ]
    assert calls == []
    assert runtime_persist_flags == [False, False]
    assert origin_persist_flags == [False]
    assert "OpenClaw gateway probe hit a fatal Docker error; aborting run instead of" in result.stdout
    assert "fallback execution." in result.stdout
    assert "Falling back to compose run with dependencies" not in result.stdout
    assert "Agent 'prindleclaw' not found" not in result.stdout


def test_openclaw_runtime_defaults_persist_false_does_not_modify_env(tmp_path: Path):
    compose_dir = tmp_path
    env_path = compose_dir / ".env"
    env_path.write_text("OPENCLAW_IMAGE=openclaw:local\n")
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"gateway": {"auth": {"token": "1234567890abcdef1234567890abcdef"}}}, indent=2) + "\n"
    )
    original_env = env_path.read_text()
    run_env: dict[str, str] = {}

    _ensure_openclaw_runtime_defaults(compose_dir, run_env, persist=False)

    assert env_path.read_text() == original_env
    assert run_env["OPENCLAW_IMAGE"] == "openclaw:local"
    assert run_env["OPENCLAW_GATEWAY_TOKEN"] == "1234567890abcdef1234567890abcdef"


def test_openclaw_gateway_allowed_origins_persist_false_does_not_modify_config(tmp_path: Path):
    compose_dir = tmp_path
    (compose_dir / ".env").write_text("OPENCLAW_GATEWAY_BIND=0.0.0.0\nOPENCLAW_GATEWAY_PORT=18789\n")
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True)
    cfg_path = cfg_dir / "openclaw.json"
    cfg_path.write_text(json.dumps({"gateway": {}}, indent=2) + "\n")
    before = cfg_path.read_text()

    _ensure_openclaw_gateway_allowed_origins(compose_dir, persist=False)

    assert cfg_path.read_text() == before


def test_run_openclaw_status_aborts_when_prereq_startup_fails(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "openclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "openclaw": {
                    "name": "openclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    startup_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    compose_run_calls: list[list[str]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        startup_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return False

    def _unexpected_subprocess_run(cmd, **kwargs):
        compose_run_calls.append(list(cmd))
        raise AssertionError("run command should not execute when prerequisite startup fails")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main.subprocess.run", _unexpected_subprocess_run)

    result = runner.invoke(app, ["run", "openclaw", "status"])

    assert result.exit_code == 1
    assert "Ensuring prerequisite services are ready: openclaw-gateway" in result.stdout
    assert startup_calls == [(install_dir, ["openclaw-gateway"], 120, {})]
    assert compose_run_calls == []


def test_run_openclaw_alias_status_aborts_when_prereq_startup_fails(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "alt-armorer"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "isolated" / "prindleclaw"
    install_dir.mkdir(parents=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                "prindleclaw": {
                    "name": "prindleclaw",
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [],
                }
            }
        )
    )

    known_spec = SimpleNamespace(
        id="openclaw",
        run_commands=[
            SimpleNamespace(
                label="Gateway status",
                service="openclaw-cli",
                command="status",
                category="run",
                description="Check gateway status quickly.",
            )
        ],
        compose_subdir=".",
        startup_timeout_seconds=120,
        startup_services=["openclaw-gateway"],
        update_channel="stable",
        monitor_targets=[],
        monitoring_mode="hybrid",
    )

    startup_calls: list[tuple[Path, list[str], int, dict[str, str]]] = []
    compose_run_calls: list[list[str]] = []

    def _fake_ensure_startup_services_ready(
        compose_dir: Path,
        startup_services: list[str],
        timeout_seconds: int = 90,
        runtime_env: dict[str, str] | None = None,
    ) -> bool:
        startup_calls.append((compose_dir, list(startup_services), int(timeout_seconds), dict(runtime_env or {})))
        return False

    def _unexpected_subprocess_run(cmd, **kwargs):
        compose_run_calls.append(list(cmd))
        raise AssertionError("run command should not execute when prerequisite startup fails")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CONFIG_DIR", str(config_dir))
    monkeypatch.setattr("armorer.main.ConfigManager", lambda: MagicMock())
    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    monkeypatch.setattr("armorer.main._resolve_install_path", lambda path, to_host=False: Path(path))
    monkeypatch.setattr("armorer.main._ensure_runtime_path_env_defaults", lambda install_dir: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_local_images", lambda install_dir, runtime_env=None: None)
    monkeypatch.setattr("armorer.main._ensure_openclaw_gateway_allowed_origins", lambda compose_dir, persist=True: None)
    monkeypatch.setattr("armorer.main._build_runtime_env", lambda install_dir: {})
    monkeypatch.setattr("armorer.main._ensure_openclaw_runtime_defaults", lambda compose_dir, run_env, persist=True: None)
    monkeypatch.setattr("armorer.main._reconcile_openclaw_gateway_token", lambda compose_dir, run_env: None)
    monkeypatch.setattr("armorer.main._ensure_runtime_security_approvals", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._prefill_from_vault_checklist", lambda *args, **kwargs: None)
    monkeypatch.setattr("armorer.main._ensure_startup_services_ready", _fake_ensure_startup_services_ready)
    monkeypatch.setattr("armorer.main.subprocess.run", _unexpected_subprocess_run)

    result = runner.invoke(app, ["run", "prindleclaw", "status"])

    assert result.exit_code == 1
    assert "Ensuring prerequisite services are ready: openclaw-gateway" in result.stdout
    assert startup_calls == [(install_dir, ["openclaw-gateway"], 120, {})]
    assert compose_run_calls == []
    assert "Agent 'prindleclaw' not found" not in result.stdout


def _prepare_openclaw_runtime_registry(
    config_dir: Path,
    install_dir: Path,
    *,
    alias: str = "openclaw",
) -> None:
    install_dir.mkdir(parents=True, exist_ok=True)
    (install_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
        "  openclaw-gateway:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (config_dir / "registry.json").write_text(
        json.dumps(
            {
                alias: {
                    "name": alias,
                    "install_path": str(install_dir),
                    "agent_id": "openclaw",
                    "run_commands": [
                        {
                            "label": "Gateway status",
                            "service": "openclaw-cli",
                            "command": "status",
                            "category": "run",
                            "description": "Check gateway status quickly.",
                        }
                    ],
                }
            }
        )
    )


def _run_openclaw_status_subprocess(
    config_dir: Path,
    bin_dir: Path,
    *,
    alias: str = "openclaw",
) -> subprocess.CompletedProcess:
    inherited = os.environ
    env = {
        "HOME": inherited.get("HOME", ""),
        "PATH": inherited.get("PATH", ""),
        "TMPDIR": inherited.get("TMPDIR", "/tmp"),
        "LANG": inherited.get("LANG", "C.UTF-8"),
    }
    env["CONFIG_DIR"] = str(config_dir)
    env["ARMORER_SANDBOX"] = "0"
    env["ARMORER_DISABLE_GATEWAY_AUTOSTART"] = "1"
    env["ARMORER_DISABLE_OTEL_AUTOSTART"] = "1"
    env["PYTHONPATH"] = str(Path(__file__).resolve().parents[1] / "src")
    env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
    return subprocess.run(
        [sys.executable, "-m", "armorer.main", "run", alias, "status"],
        cwd=Path(__file__).resolve().parents[1],
        env=env,
        capture_output=True,
        text=True,
        timeout=90,
    )


def test_run_openclaw_status_end_to_end_with_fake_docker_binary(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "installed" / "openclaw"
    _prepare_openclaw_runtime_registry(config_dir, install_dir)

    bin_dir = tmp_path / "bin"
    bin_dir.mkdir(parents=True)
    docker_bin = bin_dir / "docker"
    docker_bin.write_text(
        "#!/usr/bin/env bash\n"
        "set -euo pipefail\n"
        "if [[ \"${1:-}\" != \"compose\" ]]; then\n"
        "  echo \"unsupported docker command\" >&2\n"
        "  exit 2\n"
        "fi\n"
        "shift\n"
        "sub=\"${1:-}\"\n"
        "shift || true\n"
        "case \"$sub\" in\n"
        "  up)\n"
        "    exit 0\n"
        "    ;;\n"
        "  ps)\n"
        "    if [[ \"$*\" == *\"--format json\"* || \"$*\" == *\"--format=json\"* ]]; then\n"
        "      printf '[{\"Service\":\"openclaw-gateway\",\"State\":\"running\",\"Status\":\"Up 2 seconds\"}]\\n'\n"
        "      exit 0\n"
        "    fi\n"
        "    printf 'NAME                SERVICE            STATUS\\n'\n"
        "    printf 'openclaw-gateway    openclaw-gateway   Up 2 seconds\\n'\n"
        "    exit 0\n"
        "    ;;\n"
        "  exec)\n"
        "    args=\"$*\"\n"
        "    if [[ \"$args\" == *\"gateway probe\"* ]]; then\n"
        "      echo \"reachable: yes\"\n"
        "      exit 0\n"
        "    fi\n"
        "    if [[ \"$args\" == *\"dist/index.js status\"* ]]; then\n"
        "      echo \"OpenClaw status: healthy\"\n"
        "      exit 0\n"
        "    fi\n"
        "    exit 0\n"
        "    ;;\n"
        "  run)\n"
        "    args=\"$*\"\n"
        "    if [[ \"$args\" == *\" status\"* ]]; then\n"
        "      echo \"OpenClaw status: healthy\"\n"
        "    fi\n"
        "    exit 0\n"
        "    ;;\n"
        "  logs|config|down)\n"
        "    exit 0\n"
        "    ;;\n"
        "  *)\n"
        "    echo \"unsupported docker compose subcommand: $sub\" >&2\n"
        "    exit 2\n"
        "    ;;\n"
        "esac\n"
    )
    docker_bin.chmod(0o755)

    result = _run_openclaw_status_subprocess(config_dir, bin_dir)

    output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
    assert result.returncode == 0, output
    assert "Ensuring prerequisite services are ready: openclaw-gateway" in output
    assert "Using gateway exec path for OpenClaw" in output
    assert "OpenClaw status: healthy" in output
    assert "Docker daemon access is required for this command." not in output


def test_run_openclaw_alias_status_end_to_end_with_fake_docker_binary(tmp_path: Path, monkeypatch):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "installed" / "prindleclaw"
    _prepare_openclaw_runtime_registry(config_dir, install_dir, alias="prindleclaw")

    bin_dir = tmp_path / "bin"
    bin_dir.mkdir(parents=True)
    docker_bin = bin_dir / "docker"
    docker_bin.write_text(
        "#!/usr/bin/env bash\n"
        "set -euo pipefail\n"
        "if [[ \"${1:-}\" != \"compose\" ]]; then\n"
        "  echo \"unsupported docker command\" >&2\n"
        "  exit 2\n"
        "fi\n"
        "shift\n"
        "sub=\"${1:-}\"\n"
        "shift || true\n"
        "case \"$sub\" in\n"
        "  up)\n"
        "    exit 0\n"
        "    ;;\n"
        "  ps)\n"
        "    if [[ \"$*\" == *\"--format json\"* || \"$*\" == *\"--format=json\"* ]]; then\n"
        "      printf '[{\"Service\":\"openclaw-gateway\",\"State\":\"running\",\"Status\":\"Up 2 seconds\"}]\\n'\n"
        "      exit 0\n"
        "    fi\n"
        "    printf 'NAME                SERVICE            STATUS\\n'\n"
        "    printf 'openclaw-gateway    openclaw-gateway   Up 2 seconds\\n'\n"
        "    exit 0\n"
        "    ;;\n"
        "  exec)\n"
        "    args=\"$*\"\n"
        "    if [[ \"$args\" == *\"gateway probe\"* ]]; then\n"
        "      echo \"reachable: yes\"\n"
        "      exit 0\n"
        "    fi\n"
        "    if [[ \"$args\" == *\"dist/index.js status\"* ]]; then\n"
        "      echo \"OpenClaw status: healthy\"\n"
        "      exit 0\n"
        "    fi\n"
        "    exit 0\n"
        "    ;;\n"
        "  run)\n"
        "    args=\"$*\"\n"
        "    if [[ \"$args\" == *\" status\"* ]]; then\n"
        "      echo \"OpenClaw status: healthy\"\n"
        "    fi\n"
        "    exit 0\n"
        "    ;;\n"
        "  logs|config|down)\n"
        "    exit 0\n"
        "    ;;\n"
        "  *)\n"
        "    echo \"unsupported docker compose subcommand: $sub\" >&2\n"
        "    exit 2\n"
        "    ;;\n"
        "esac\n"
    )
    docker_bin.chmod(0o755)

    result = _run_openclaw_status_subprocess(config_dir, bin_dir, alias="prindleclaw")

    output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
    assert result.returncode == 0, output
    assert "Ensuring prerequisite services are ready: openclaw-gateway" in output
    assert "Using gateway exec path for OpenClaw" in output
    assert "OpenClaw status: healthy" in output
    assert "Agent 'prindleclaw' not found" not in output
    assert "Docker daemon access is required for this command." not in output


def test_run_openclaw_status_end_to_end_aborts_when_gateway_probe_reports_docker_socket_error(
    tmp_path: Path,
):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "installed" / "openclaw"
    _prepare_openclaw_runtime_registry(config_dir, install_dir)

    bin_dir = tmp_path / "bin"
    bin_dir.mkdir(parents=True)
    docker_bin = bin_dir / "docker"
    docker_bin.write_text(
        "#!/usr/bin/env bash\n"
        "set -euo pipefail\n"
        "if [[ \"${1:-}\" != \"compose\" ]]; then\n"
        "  echo \"unsupported docker command\" >&2\n"
        "  exit 2\n"
        "fi\n"
        "shift\n"
        "sub=\"${1:-}\"\n"
        "shift || true\n"
        "case \"$sub\" in\n"
        "  up)\n"
        "    exit 0\n"
        "    ;;\n"
        "  ps)\n"
        "    if [[ \"$*\" == *\"--format json\"* || \"$*\" == *\"--format=json\"* ]]; then\n"
        "      printf '[{\"Service\":\"openclaw-gateway\",\"State\":\"running\",\"Status\":\"Up 2 seconds\"}]\\n'\n"
        "      exit 0\n"
        "    fi\n"
        "    printf 'NAME                SERVICE            STATUS\\n'\n"
        "    printf 'openclaw-gateway    openclaw-gateway   Up 2 seconds\\n'\n"
        "    exit 0\n"
        "    ;;\n"
        "  exec)\n"
        "    args=\"$*\"\n"
        "    if [[ \"$args\" == *\"gateway probe\"* ]]; then\n"
        "      echo \"permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: connect: operation not permitted\" >&2\n"
        "      exit 1\n"
        "    fi\n"
        "    if [[ \"$args\" == *\"dist/index.js status\"* ]]; then\n"
        "      echo \"OpenClaw status: healthy\"\n"
        "      exit 0\n"
        "    fi\n"
        "    exit 0\n"
        "    ;;\n"
        "  run)\n"
        "    args=\"$*\"\n"
        "    if [[ \"$args\" == *\" status\"* ]]; then\n"
        "      echo \"OpenClaw status: healthy\"\n"
        "    fi\n"
        "    exit 0\n"
        "    ;;\n"
        "  logs|config|down)\n"
        "    exit 0\n"
        "    ;;\n"
        "  *)\n"
        "    echo \"unsupported docker compose subcommand: $sub\" >&2\n"
        "    exit 2\n"
        "    ;;\n"
        "esac\n"
    )
    docker_bin.chmod(0o755)

    result = _run_openclaw_status_subprocess(config_dir, bin_dir)

    output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
    assert result.returncode != 0
    assert "Ensuring prerequisite services are ready: openclaw-gateway" in output
    assert "OpenClaw gateway probe failed:" in output
    assert "Docker daemon access is required for this command." in output
    assert "OpenClaw gateway probe hit a fatal Docker error; aborting run instead of" in output
    assert "fallback execution." in output
    assert "Falling back to compose run with dependencies" not in output
    assert "OpenClaw status: healthy" not in output


def test_run_openclaw_alias_status_end_to_end_aborts_when_gateway_probe_reports_docker_socket_error(
    tmp_path: Path,
):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "installed" / "prindleclaw"
    _prepare_openclaw_runtime_registry(config_dir, install_dir, alias="prindleclaw")

    bin_dir = tmp_path / "bin"
    bin_dir.mkdir(parents=True)
    docker_bin = bin_dir / "docker"
    docker_bin.write_text(
        "#!/usr/bin/env bash\n"
        "set -euo pipefail\n"
        "if [[ \"${1:-}\" != \"compose\" ]]; then\n"
        "  echo \"unsupported docker command\" >&2\n"
        "  exit 2\n"
        "fi\n"
        "shift\n"
        "sub=\"${1:-}\"\n"
        "shift || true\n"
        "case \"$sub\" in\n"
        "  up)\n"
        "    exit 0\n"
        "    ;;\n"
        "  ps)\n"
        "    if [[ \"$*\" == *\"--format json\"* || \"$*\" == *\"--format=json\"* ]]; then\n"
        "      printf '[{\"Service\":\"openclaw-gateway\",\"State\":\"running\",\"Status\":\"Up 2 seconds\"}]\\n'\n"
        "      exit 0\n"
        "    fi\n"
        "    printf 'NAME                SERVICE            STATUS\\n'\n"
        "    printf 'openclaw-gateway    openclaw-gateway   Up 2 seconds\\n'\n"
        "    exit 0\n"
        "    ;;\n"
        "  exec)\n"
        "    args=\"$*\"\n"
        "    if [[ \"$args\" == *\"gateway probe\"* ]]; then\n"
        "      echo \"permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: connect: operation not permitted\" >&2\n"
        "      exit 1\n"
        "    fi\n"
        "    if [[ \"$args\" == *\"dist/index.js status\"* ]]; then\n"
        "      echo \"OpenClaw status: healthy\"\n"
        "      exit 0\n"
        "    fi\n"
        "    exit 0\n"
        "    ;;\n"
        "  run)\n"
        "    args=\"$*\"\n"
        "    if [[ \"$args\" == *\" status\"* ]]; then\n"
        "      echo \"OpenClaw status: healthy\"\n"
        "    fi\n"
        "    exit 0\n"
        "    ;;\n"
        "  logs|config|down)\n"
        "    exit 0\n"
        "    ;;\n"
        "  *)\n"
        "    echo \"unsupported docker compose subcommand: $sub\" >&2\n"
        "    exit 2\n"
        "    ;;\n"
        "esac\n"
    )
    docker_bin.chmod(0o755)

    result = _run_openclaw_status_subprocess(config_dir, bin_dir, alias="prindleclaw")

    output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
    assert result.returncode != 0
    assert "Ensuring prerequisite services are ready: openclaw-gateway" in output
    assert "OpenClaw gateway probe failed:" in output
    assert "Docker daemon access is required for this command." in output
    assert "OpenClaw gateway probe hit a fatal Docker error; aborting run instead of" in output
    assert "fallback execution." in output
    assert "Falling back to compose run with dependencies" not in output
    assert "OpenClaw status: healthy" not in output
    assert "Agent 'prindleclaw' not found" not in output


def test_run_openclaw_status_end_to_end_reports_docker_socket_hint_when_prereq_startup_fails(
    tmp_path: Path,
):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "installed" / "openclaw"
    _prepare_openclaw_runtime_registry(config_dir, install_dir)

    bin_dir = tmp_path / "bin"
    bin_dir.mkdir(parents=True)
    docker_bin = bin_dir / "docker"
    docker_bin.write_text(
        "#!/usr/bin/env bash\n"
        "set -euo pipefail\n"
        "if [[ \"${1:-}\" != \"compose\" ]]; then\n"
        "  echo \"unsupported docker command\" >&2\n"
        "  exit 2\n"
        "fi\n"
        "shift\n"
        "sub=\"${1:-}\"\n"
        "shift || true\n"
        "case \"$sub\" in\n"
        "  up)\n"
        "    echo \"permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: connect: operation not permitted\" >&2\n"
        "    exit 1\n"
        "    ;;\n"
        "  *)\n"
        "    echo \"unexpected docker compose subcommand in startup-failure test: $sub\" >&2\n"
        "    exit 2\n"
        "    ;;\n"
        "esac\n"
    )
    docker_bin.chmod(0o755)

    result = _run_openclaw_status_subprocess(config_dir, bin_dir)

    output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
    assert result.returncode != 0
    assert "Ensuring prerequisite services are ready: openclaw-gateway" in output
    assert "Failed to start prerequisite services:" in output
    assert "Docker daemon access is required for this command." in output
    assert "OpenClaw status: healthy" not in output


def test_run_openclaw_alias_status_end_to_end_reports_docker_socket_hint_when_prereq_startup_fails(
    tmp_path: Path,
):
    config_dir = tmp_path / "armorer-config"
    config_dir.mkdir(parents=True)
    install_dir = tmp_path / "installed" / "prindleclaw"
    _prepare_openclaw_runtime_registry(config_dir, install_dir, alias="prindleclaw")

    bin_dir = tmp_path / "bin"
    bin_dir.mkdir(parents=True)
    docker_bin = bin_dir / "docker"
    docker_bin.write_text(
        "#!/usr/bin/env bash\n"
        "set -euo pipefail\n"
        "if [[ \"${1:-}\" != \"compose\" ]]; then\n"
        "  echo \"unsupported docker command\" >&2\n"
        "  exit 2\n"
        "fi\n"
        "shift\n"
        "sub=\"${1:-}\"\n"
        "shift || true\n"
        "case \"$sub\" in\n"
        "  up)\n"
        "    echo \"permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: connect: operation not permitted\" >&2\n"
        "    exit 1\n"
        "    ;;\n"
        "  *)\n"
        "    echo \"unexpected docker compose subcommand in startup-failure test: $sub\" >&2\n"
        "    exit 2\n"
        "    ;;\n"
        "esac\n"
    )
    docker_bin.chmod(0o755)

    result = _run_openclaw_status_subprocess(config_dir, bin_dir, alias="prindleclaw")

    output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
    assert result.returncode != 0
    assert "Ensuring prerequisite services are ready: openclaw-gateway" in output
    assert "Failed to start prerequisite services:" in output
    assert "Docker daemon access is required for this command." in output
    assert "OpenClaw status: healthy" not in output
    assert "Agent 'prindleclaw' not found" not in output


def test_ensure_startup_services_ready_reports_docker_socket_hint(monkeypatch):
    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda *args, **kwargs: subprocess.CompletedProcess(
            args[0],
            1,
            stdout="",
            stderr=(
                'permission denied while trying to connect to the Docker daemon socket at '
                'unix:///var/run/docker.sock: connect: operation not permitted'
            ),
        ),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    ok = _ensure_startup_services_ready(Path("/tmp/demo"), ["openclaw-gateway"], runtime_env={})

    assert ok is False
    assert any("Failed to start prerequisite services:" in line for line in printed)
    assert any("Docker daemon access is required for this command." in line for line in printed)


def test_ensure_startup_services_ready_reports_docker_cli_missing_hint(monkeypatch):
    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda *args, **kwargs: subprocess.CompletedProcess(
            args[0],
            1,
            stdout="",
            stderr="docker: command not found",
        ),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    ok = _ensure_startup_services_ready(Path("/tmp/demo"), ["openclaw-gateway"], runtime_env={})

    assert ok is False
    assert any("Failed to start prerequisite services:" in line for line in printed)
    assert any("Docker CLI is required for this command." in line for line in printed)


def test_ensure_startup_services_ready_reports_windows_docker_cli_missing_hint(monkeypatch):
    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda *args, **kwargs: subprocess.CompletedProcess(
            args[0],
            1,
            stdout="",
            stderr="'docker' is not recognized as an internal or external command",
        ),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    ok = _ensure_startup_services_ready(Path("/tmp/demo"), ["openclaw-gateway"], runtime_env={})

    assert ok is False
    assert any("Failed to start prerequisite services:" in line for line in printed)
    assert any("Docker CLI is required for this command." in line for line in printed)


def test_ensure_startup_services_ready_reports_windows_docker_daemon_unavailable_hint(monkeypatch):
    printed: list[str] = []

    monkeypatch.setattr(
        "armorer.main.subprocess.run",
        lambda *args, **kwargs: subprocess.CompletedProcess(
            args[0],
            1,
            stdout="",
            stderr=(
                "error during connect: This error may indicate that the docker daemon is not running: "
                "open //./pipe/docker_engine: The system cannot find the file specified."
            ),
        ),
    )
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    ok = _ensure_startup_services_ready(Path("/tmp/demo"), ["openclaw-gateway"], runtime_env={})

    assert ok is False
    assert any("Failed to start prerequisite services:" in line for line in printed)
    assert any("Docker daemon access is required for this command." in line for line in printed)


def test_ensure_startup_services_ready_handles_missing_docker_binary_exception(monkeypatch):
    printed: list[str] = []

    def _missing_docker(*args, **kwargs):
        raise FileNotFoundError(2, "No such file or directory", "docker")

    monkeypatch.setattr("armorer.main.subprocess.run", _missing_docker)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    ok = _ensure_startup_services_ready(Path("/tmp/demo"), ["openclaw-gateway"], runtime_env={})

    assert ok is False
    assert any("Failed to start prerequisite services:" in line for line in printed)
    assert any("Docker CLI is required for this command." in line for line in printed)


def test_ensure_startup_services_ready_reports_timeout_when_compose_up_hangs(monkeypatch):
    printed: list[str] = []
    seen_timeouts: list[int] = []

    def _timeout(*args, **kwargs):
        timeout = int(kwargs.get("timeout", 0) or 0)
        seen_timeouts.append(timeout)
        raise subprocess.TimeoutExpired(cmd=args[0], timeout=timeout)

    monkeypatch.setattr("armorer.main.subprocess.run", _timeout)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    ok = _ensure_startup_services_ready(
        Path("/tmp/demo"),
        ["openclaw-gateway"],
        timeout_seconds=42,
        runtime_env={},
    )

    assert ok is False
    assert seen_timeouts == [42]
    assert any("Failed to start prerequisite services:" in line for line in printed)
    assert any("timed out after 42s" in line for line in printed)


def test_ensure_startup_services_ready_aborts_early_when_compose_ps_reports_docker_socket_error(monkeypatch):
    printed: list[str] = []
    sleep_calls: list[float] = []
    calls: list[list[str]] = []

    def _fake_run(cmd, *args, **kwargs):
        calls.append(list(cmd))
        if cmd[:3] == ["docker", "compose", "up"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:4] == ["docker", "compose", "ps", "-a"]:
            return subprocess.CompletedProcess(
                cmd,
                1,
                stdout="",
                stderr=(
                    "permission denied while trying to connect to the Docker daemon socket at "
                    "unix:///var/run/docker.sock: connect: operation not permitted"
                ),
            )
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))
    monkeypatch.setattr("armorer.main.time.sleep", lambda seconds: sleep_calls.append(float(seconds)))

    ok = _ensure_startup_services_ready(Path("/tmp/demo"), ["openclaw-gateway"], runtime_env={})

    assert ok is False
    assert calls[:2] == [
        ["docker", "compose", "up", "-d", "openclaw-gateway"],
        ["docker", "compose", "ps", "-a", "--format", "json"],
    ]
    assert sleep_calls == []
    assert any("Failed to inspect prerequisite services:" in line for line in printed)
    assert any("Docker daemon access is required for this command." in line for line in printed)


def test_ensure_startup_services_ready_aborts_early_when_compose_ps_raises_missing_docker_binary(monkeypatch):
    printed: list[str] = []
    sleep_calls: list[float] = []
    calls: list[list[str]] = []

    def _fake_run(cmd, *args, **kwargs):
        calls.append(list(cmd))
        if cmd[:3] == ["docker", "compose", "up"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:4] == ["docker", "compose", "ps", "-a"]:
            raise FileNotFoundError(2, "No such file or directory", "docker")
        raise AssertionError(f"Unexpected command: {cmd}")

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))
    monkeypatch.setattr("armorer.main.time.sleep", lambda seconds: sleep_calls.append(float(seconds)))

    ok = _ensure_startup_services_ready(Path("/tmp/demo"), ["openclaw-gateway"], runtime_env={})

    assert ok is False
    assert calls[:2] == [
        ["docker", "compose", "up", "-d", "openclaw-gateway"],
        ["docker", "compose", "ps", "-a", "--format", "json"],
    ]
    assert sleep_calls == []
    assert any("Failed to inspect prerequisite services:" in line for line in printed)
    assert any("Docker CLI is required for this command." in line for line in printed)


def test_wait_openclaw_gateway_reachable_returns_true_when_probe_reports_reachable(monkeypatch):
    calls: list[list[str]] = []

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0, stdout="reachable: yes\n", stderr="")

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main.time.sleep", lambda *_args, **_kwargs: None)

    ok = _wait_openclaw_gateway_reachable(Path("/tmp/demo"), {}, timeout_seconds=60)

    assert ok is True
    assert getattr(_wait_openclaw_gateway_reachable, "last_failure_is_fatal", False) is False
    assert calls == [[
        "docker",
        "compose",
        "exec",
        "-T",
        "openclaw-gateway",
        "node",
        "dist/index.js",
        "gateway",
        "probe",
    ]]


def test_wait_openclaw_gateway_reachable_aborts_early_on_fatal_docker_daemon_error(monkeypatch):
    calls: list[list[str]] = []
    sleep_calls: list[int] = []
    printed: list[str] = []

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(
            cmd,
            1,
            stdout="",
            stderr=(
                "permission denied while trying to connect to the Docker daemon socket at "
                "unix:///var/run/docker.sock: connect: operation not permitted"
            ),
        )

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main.time.sleep", lambda seconds: sleep_calls.append(int(seconds)))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    ok = _wait_openclaw_gateway_reachable(Path("/tmp/demo"), {}, timeout_seconds=60)

    assert ok is False
    assert getattr(_wait_openclaw_gateway_reachable, "last_failure_is_fatal", False) is True
    assert len(calls) == 1
    assert sleep_calls == []
    assert any("OpenClaw gateway probe failed:" in line for line in printed)
    assert any("Docker daemon access is required for this command." in line for line in printed)


def test_wait_openclaw_gateway_reachable_aborts_early_on_docker_cli_missing_error_output(monkeypatch):
    calls: list[list[str]] = []
    sleep_calls: list[int] = []
    printed: list[str] = []

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="docker: command not found")

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main.time.sleep", lambda seconds: sleep_calls.append(int(seconds)))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    ok = _wait_openclaw_gateway_reachable(Path("/tmp/demo"), {}, timeout_seconds=60)

    assert ok is False
    assert getattr(_wait_openclaw_gateway_reachable, "last_failure_is_fatal", False) is True
    assert len(calls) == 1
    assert sleep_calls == []
    assert any("OpenClaw gateway probe failed:" in line for line in printed)
    assert any("Docker CLI is required for this command." in line for line in printed)


def test_wait_openclaw_gateway_reachable_aborts_early_on_missing_docker_binary_exception(monkeypatch):
    calls: list[list[str]] = []
    sleep_calls: list[int] = []
    printed: list[str] = []

    def _missing_docker(cmd, **kwargs):
        calls.append(list(cmd))
        raise FileNotFoundError(2, "No such file or directory", "docker")

    monkeypatch.setattr("armorer.main.subprocess.run", _missing_docker)
    monkeypatch.setattr("armorer.main.time.sleep", lambda seconds: sleep_calls.append(int(seconds)))
    monkeypatch.setattr("armorer.main.console.print", lambda *args, **kwargs: printed.append(" ".join(str(a) for a in args)))

    ok = _wait_openclaw_gateway_reachable(Path("/tmp/demo"), {}, timeout_seconds=60)

    assert ok is False
    assert getattr(_wait_openclaw_gateway_reachable, "last_failure_is_fatal", False) is True
    assert len(calls) == 1
    assert sleep_calls == []
    assert any("OpenClaw gateway probe failed:" in line for line in printed)
    assert any("Docker CLI is required for this command." in line for line in printed)


def test_wait_openclaw_gateway_reachable_retries_after_probe_timeout(monkeypatch):
    calls: list[list[str]] = []
    seen_timeouts: list[int] = []

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        timeout = int(kwargs.get("timeout", 0) or 0)
        seen_timeouts.append(timeout)
        if len(calls) == 1:
            raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout)
        return subprocess.CompletedProcess(cmd, 0, stdout="reachable: yes\n", stderr="")

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main.time.sleep", lambda *_args, **_kwargs: None)

    ok = _wait_openclaw_gateway_reachable(Path("/tmp/demo"), {}, timeout_seconds=60)

    assert ok is True
    assert getattr(_wait_openclaw_gateway_reachable, "last_failure_is_fatal", False) is False
    assert len(calls) == 2
    assert all(timeout > 0 for timeout in seen_timeouts)
    assert all(timeout <= 8 for timeout in seen_timeouts)


def test_wait_openclaw_gateway_reachable_resets_fatal_flag_after_subsequent_success(monkeypatch):
    calls: list[list[str]] = []

    def _fatal_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(
            cmd,
            1,
            stdout="",
            stderr=(
                "permission denied while trying to connect to the Docker daemon socket at "
                "unix:///var/run/docker.sock: connect: operation not permitted"
            ),
        )

    def _success_run(cmd, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0, stdout="reachable: yes\n", stderr="")

    monkeypatch.setattr("armorer.main.subprocess.run", _fatal_run)
    monkeypatch.setattr("armorer.main.time.sleep", lambda *_args, **_kwargs: None)
    assert _wait_openclaw_gateway_reachable(Path("/tmp/demo"), {}, timeout_seconds=60) is False
    assert getattr(_wait_openclaw_gateway_reachable, "last_failure_is_fatal", False) is True

    monkeypatch.setattr("armorer.main.subprocess.run", _success_run)
    assert _wait_openclaw_gateway_reachable(Path("/tmp/demo"), {}, timeout_seconds=60) is True
    assert getattr(_wait_openclaw_gateway_reachable, "last_failure_is_fatal", False) is False
    assert len(calls) == 2


def test_wait_openclaw_gateway_reachable_caps_wait_budget_for_large_timeout(monkeypatch):
    calls: list[list[str]] = []
    seen_probe_timeouts: list[int] = []
    now = {"value": 0.0}

    def _fake_run(cmd, **kwargs):
        calls.append(list(cmd))
        seen_probe_timeouts.append(int(kwargs.get("timeout", 0) or 0))
        return subprocess.CompletedProcess(cmd, 0, stdout="reachable: no\n", stderr="")

    def _fake_time() -> float:
        return float(now["value"])

    def _fake_sleep(_seconds: float) -> None:
        now["value"] = float(now["value"]) + 30.0

    monkeypatch.setattr("armorer.main.subprocess.run", _fake_run)
    monkeypatch.setattr("armorer.main.time.time", _fake_time)
    monkeypatch.setattr("armorer.main.time.sleep", _fake_sleep)

    ok = _wait_openclaw_gateway_reachable(Path("/tmp/demo"), {}, timeout_seconds=600)

    assert ok is False
    assert getattr(_wait_openclaw_gateway_reachable, "last_failure_is_fatal", False) is False
    assert len(calls) == 3
    assert all(timeout > 0 for timeout in seen_probe_timeouts)
    assert all(timeout <= 8 for timeout in seen_probe_timeouts)
