import json
import os
import subprocess
import sys
import time
from pathlib import Path
from types import SimpleNamespace

import pytest
from typer.testing import CliRunner

from armorer.config import ConfigManager
from armorer.core import ArmorerCoreHandler, OpenClawSetupExecutionError, SupersededTaskError
from armorer.main import app
from armorer.registry import AppMetadata, AppRegistry


runner = CliRunner()


def _setup(tmp_path: Path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OLLAMA_BASE_URL",
        "ARMORER_SKILL_SCAN_PATHS",
        "ARMORER_TELEGRAM_ADMIN_USER_IDS",
        "ARMORER_TELEGRAM_ADMIN_CHAT_IDS",
        "ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS",
        "TELEGRAM_ALERT_CHAT_ID",
    ):
        monkeypatch.delenv(key, raising=False)
    config_dir = tmp_path / "armorer"
    config_dir.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(AppRegistry, "REGISTRY_FILE", config_dir / "registry.json")
    monkeypatch.setattr(ConfigManager, "VAULT_FILE", config_dir / "vault.json")
    monkeypatch.setattr(ConfigManager, "CONFIG_DIR", config_dir)
    (config_dir / "vault.json").write_text(json.dumps({"keys": {}, "secrets": {}}, indent=2))
    return config_dir


def _seed_existing_openclaw_runtime(handler: ArmorerCoreHandler, provider: str = "openai") -> None:
    handler.config_manager.set_key("MODEL_PROVIDER", provider)
    handler.config_manager.set_key("Armorer_MODEL", "existing-model")
    if provider == "openai":
        handler.config_manager.set_key("OPENAI_API_KEY", "existing-openai-key")
    elif provider == "anthropic":
        handler.config_manager.set_key("ANTHROPIC_API_KEY", "existing-anthropic-key")
    elif provider == "openrouter":
        handler.config_manager.set_key("OPENROUTER_API_KEY", "existing-openrouter-key")
    elif provider == "gemini":
        handler.config_manager.set_key("GEMINI_API_KEY", "existing-gemini-key")


def _normalize_setup_command_args(cmd) -> list[str]:
    args = list(cmd)
    if args[:3] == [sys.executable, "-m", "armorer.main"]:
        return ["armorer", *args[3:]]
    return args


def _materialize_skill_dirs(compose_dir: Path, *paths: str) -> None:
    for raw_path in paths:
        path_text = str(raw_path or "").strip()
        if not path_text:
            continue
        candidate = Path(path_text).expanduser()
        if not candidate.is_absolute():
            candidate = compose_dir / candidate
        candidate.mkdir(parents=True, exist_ok=True)


@pytest.fixture(autouse=True)
def _fake_telegram_gateway(monkeypatch):
    from armorer import core as core_mod

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "armorer_test_bot"}

        def get_chat(self, chat_id: str):
            return {"id": str(chat_id), "type": "supergroup", "title": "Armorer Test Chat"}

        def send_message(self, *args, **kwargs):
            return {"ok": True}

        def send_document(self, *args, **kwargs):
            return {"ok": True}

    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)


def test_secure_gate_requests_target_selection_for_ambiguous_high_risk_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(AppMetadata(name="openclaw", install_path=str(tmp_path / "installed" / "openclaw")))

    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    inbox = config_dir / "inbox"
    task_path = inbox / "t1.json"
    task_path.write_text(
        json.dumps(
            {
                "id": "t1",
                "prompt": "restart it now",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1", "user_id": "1"},
            }
        )
    )

    handler._process_inbox_task(str(task_path))
    out = json.loads((config_dir / "outbox" / "t1.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert "openclaw" in payload.get("ui_action", {}).get("options", [])
    assert "target assistant is unclear" in payload.get("message", "").lower()


def test_secure_gate_requests_target_selection_for_openclaw_fallback_when_registry_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_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"
    )

    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    inbox = config_dir / "inbox"
    task_path = inbox / "t1.json"
    task_path.write_text(
        json.dumps(
            {
                "id": "t1",
                "prompt": "restart it now",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1", "user_id": "1"},
            }
        )
    )

    handler._process_inbox_task(str(task_path))
    out = json.loads((config_dir / "outbox" / "t1.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert "openclaw" in payload.get("ui_action", {}).get("options", [])
    assert "target assistant is unclear" in payload.get("message", "").lower()


def test_secure_gate_resumes_after_target_selection(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(AppMetadata(name="openclaw", install_path=str(tmp_path / "installed" / "openclaw")))
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    # First pass: generate pending selection decision.
    (inbox / "t1.json").write_text(
        json.dumps({"id": "t1", "prompt": "restart it now", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "t1.json"))

    # Second pass: user provides selection.
    (inbox / "t2.json").write_text(
        json.dumps({"id": "t2", "prompt": "UI selection: openclaw", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "t2.json"))

    assert "Target assistant selected by operator: openclaw" in captured.get("user_input", "")
    out = json.loads((config_dir / "outbox" / "t2.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("message") == "ok"


def test_secure_gate_target_selection_can_be_cancelled(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(AppMetadata(name="openclaw", install_path=str(tmp_path / "installed" / "openclaw")))
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "t1.json").write_text(
        json.dumps({"id": "t1", "prompt": "restart it now", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "t1.json"))

    (inbox / "t2.json").write_text(
        json.dumps({"id": "t2", "prompt": "cancel", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "t2.json"))

    out = json.loads((config_dir / "outbox" / "t2.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("message") == "Cancelled. No high-risk action was executed."

    (inbox / "t3.json").write_text(
        json.dumps({"id": "t3", "prompt": "tell me a joke", "source": "cli", "timestamp": 2, "metadata": {"channel_id": "1"}})
    )
    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    handler._process_inbox_task(str(inbox / "t3.json"))
    assert captured.get("user_input") == "tell me a joke"


def test_core_task_cleanup_tolerates_missing_queue_files(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    inbox = config_dir / "inbox"
    task_path = inbox / "cleanup-race.json"
    task_path.write_text(
        json.dumps(
            {
                "id": "cleanup-race",
                "prompt": "status",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, status_file=None, *args, **kwargs):
        Path(task_path).unlink(missing_ok=True)
        if status_file is not None:
            Path(status_file).unlink(missing_ok=True)
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    handler._process_inbox_task(str(task_path))

    out = json.loads((config_dir / "outbox" / "cleanup-race.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("message") == "ok"


def test_core_unreadable_json_task_is_dropped_after_bounded_retries(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    task_path = inbox / "bad-json.json"
    task_path.write_text("{")
    stale = time.time() - 10
    os.utime(task_path, (stale, stale))

    handler._process_inbox_task(str(task_path))
    assert task_path.exists()
    assert not (config_dir / "outbox" / "bad-json.json").exists()

    handler._process_inbox_task(str(task_path))
    assert task_path.exists()
    assert not (config_dir / "outbox" / "bad-json.json").exists()

    handler._process_inbox_task(str(task_path))
    assert not task_path.exists()

    out = json.loads((config_dir / "outbox" / "bad-json.json").read_text())
    payload = json.loads(str(out["response"]))
    assert "unreadable json after multiple retries" in str(payload.get("message", "")).lower()


def test_core_unreadable_json_retries_reset_when_task_file_changes(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    task_path = inbox / "bad-json-reset.json"
    task_path.write_text("{")
    stale = time.time() - 10
    os.utime(task_path, (stale, stale))

    handler._process_inbox_task(str(task_path))
    assert task_path.exists()

    future = time.time() + 1000
    os.utime(task_path, (future, future))

    handler._process_inbox_task(str(task_path))
    handler._process_inbox_task(str(task_path))
    handler._process_inbox_task(str(task_path))

    assert task_path.exists()
    assert not (config_dir / "outbox" / "bad-json-reset.json").exists()


def test_core_unreadable_json_retries_reset_when_size_changes_even_if_mtime_constant(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    task_path = inbox / "bad-json-size-reset.json"
    task_path.write_text("{")
    stale = time.time() - 10
    os.utime(task_path, (stale, stale))

    handler._process_inbox_task(str(task_path))
    assert task_path.exists()

    task_path.write_text("{{")
    os.utime(task_path, (stale, stale))

    handler._process_inbox_task(str(task_path))
    handler._process_inbox_task(str(task_path))

    assert task_path.exists()
    assert not (config_dir / "outbox" / "bad-json-size-reset.json").exists()


def test_core_unreadable_json_retries_reset_when_ctime_changes_even_if_mtime_and_size_constant(
    tmp_path: Path,
    monkeypatch,
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    task_path = inbox / "bad-json-ctime-reset.json"
    task_path.write_text("{")
    stale = time.time() - 10
    os.utime(task_path, (stale, stale))

    handler._process_inbox_task(str(task_path))
    key = str(task_path)
    assert int(handler._unreadable_task_retries[key]["attempt"]) == 1

    task_path.write_text("[")
    os.utime(task_path, (stale, stale))

    handler._process_inbox_task(str(task_path))
    assert int(handler._unreadable_task_retries[key]["attempt"]) == 1
    assert task_path.exists()
    assert not (config_dir / "outbox" / "bad-json-ctime-reset.json").exists()


def test_core_unreadable_json_recovers_when_task_file_becomes_valid(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    task_path = inbox / "bad-json-recover.json"
    task_path.write_text("{")
    stale = time.time() - 10
    os.utime(task_path, (stale, stale))

    handler._process_inbox_task(str(task_path))
    assert task_path.exists()

    task_path.write_text(
        json.dumps(
            {
                "id": "recover1",
                "prompt": "status",
                "source": "cli",
                "timestamp": 1,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    handler._process_inbox_task(str(task_path))

    out = json.loads((config_dir / "outbox" / "recover1.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("message") == "ok"
    assert captured.get("user_input") == "status"
    assert str(task_path) not in handler._unreadable_task_retries


def test_core_prune_unreadable_task_retries_drops_missing_and_stale_entries(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    keep_file = inbox / "keep.json"
    keep_file.write_text("{}")
    stale_file = inbox / "stale.json"
    stale_file.write_text("{}")
    missing_file = inbox / "missing.json"

    now = time.time()
    handler._unreadable_task_retries = {
        str(keep_file): {"attempt": 1, "mtime_ns": 1, "ctime_ns": 1, "size": 2, "updated_at": now},
        str(stale_file): {"attempt": 2, "mtime_ns": 1, "ctime_ns": 1, "size": 2, "updated_at": now - 7200},
        str(missing_file): {"attempt": 1, "mtime_ns": 1, "ctime_ns": 1, "size": 2, "updated_at": now},
    }

    handler._prune_unreadable_task_retries(max_age_seconds=300)

    assert list(handler._unreadable_task_retries.keys()) == [str(keep_file)]


def test_core_poll_inbox_prunes_unreadable_retry_state(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    missing_file = config_dir / "inbox" / "missing-prune.json"
    now = time.time()
    handler._unreadable_task_retries = {
        str(missing_file): {"attempt": 1, "mtime_ns": 1, "ctime_ns": 1, "size": 2, "updated_at": now - 7200}
    }

    seen: list[str] = []
    monkeypatch.setattr(handler, "_process_inbox_task", lambda task_path: seen.append(task_path), raising=True)
    monkeypatch.setattr(handler, "_poll_otel_events", lambda: None, raising=True)
    monkeypatch.setattr(handler, "_maybe_send_daily_report", lambda: None, raising=True)

    handler._poll_inbox()

    assert str(missing_file) not in handler._unreadable_task_retries
    assert seen == []


def test_core_error_fallback_uses_task_file_stem_when_id_not_available(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    task_path = inbox / "fallback-id.json"
    task_path.write_text(
        json.dumps(
            {
                "prompt": "status",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    monkeypatch.setattr(
        handler,
        "_verify_task_signature",
        lambda _task: (_ for _ in ()).throw(RuntimeError("signature verifier crashed")),
        raising=True,
    )

    handler._process_inbox_task(str(task_path))

    fallback = json.loads((config_dir / "outbox" / "fallback-id.json").read_text())
    assert fallback.get("id") == "fallback-id"
    payload = json.loads(str(fallback.get("response") or "{}"))
    assert payload.get("message", "").startswith("Error: signature verifier crashed")
    assert not (config_dir / "outbox" / "None.json").exists()
    assert not task_path.exists()


def test_core_error_fallback_redacts_secret_like_exception_text(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    task_path = inbox / "fallback-redact.json"
    task_path.write_text(
        json.dumps(
            {
                "prompt": "status",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    leaked_secret = "sk-abcdefghijklmnopqrstuvwxyz0123456789"
    monkeypatch.setattr(
        handler,
        "_verify_task_signature",
        lambda _task: (_ for _ in ()).throw(RuntimeError(f"verifier failed: {leaked_secret}")),
        raising=True,
    )

    handler._process_inbox_task(str(task_path))

    fallback = json.loads((config_dir / "outbox" / "fallback-redact.json").read_text())
    payload = json.loads(str(fallback.get("response") or "{}"))
    message = str(payload.get("message") or "")
    assert leaked_secret not in message
    assert "[REDACTED_OPENAI_KEY]" in message


def test_core_error_fallback_redacts_secret_like_exception_text_when_guard_sanitizer_fails(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    task_path = inbox / "fallback-redact-guard-fail.json"
    task_path.write_text(
        json.dumps(
            {
                "prompt": "status",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    monkeypatch.setattr(
        handler,
        "_verify_task_signature",
        lambda _task: (_ for _ in ()).throw(RuntimeError(f"verifier failed: {leaked_key} {leaked_token}")),
        raising=True,
    )
    monkeypatch.setattr(
        handler.guard,
        "inspect_operator",
        lambda _text: (_ for _ in ()).throw(RuntimeError("guard offline")),
        raising=True,
    )

    handler._process_inbox_task(str(task_path))

    fallback = json.loads((config_dir / "outbox" / "fallback-redact-guard-fail.json").read_text())
    payload = json.loads(str(fallback.get("response") or "{}"))
    message = str(payload.get("message") or "")
    assert leaked_key not in message
    assert leaked_token not in message
    assert "[REDACTED_OPENAI_KEY]" in message
    assert "[REDACTED_TELEGRAM_TOKEN]" in message


def test_core_error_fallback_uses_remote_output_sanitizer_when_source_is_remote(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    task_path = inbox / "fallback-remote.json"
    task_path.write_text(
        json.dumps(
            {
                "prompt": "status",
                "source": "telegram",
                "timestamp": 0,
                "metadata": {"channel_id": "-100123", "user_id": "42"},
            }
        )
    )

    monkeypatch.setattr(
        handler,
        "_verify_task_signature",
        lambda _task: (_ for _ in ()).throw(RuntimeError("signature verification crashed")),
        raising=True,
    )

    calls = {"operator": 0, "output": 0}

    def _inspect_operator(_text: str):
        calls["operator"] += 1
        return SimpleNamespace(sanitized_text="operator-sanitized")

    def _inspect_output(_text: str):
        calls["output"] += 1
        return SimpleNamespace(sanitized_text="output-sanitized")

    monkeypatch.setattr(handler.guard, "inspect_operator", _inspect_operator, raising=True)
    monkeypatch.setattr(handler.guard, "inspect_output", _inspect_output, raising=True)

    handler._process_inbox_task(str(task_path))

    fallback = json.loads((config_dir / "outbox" / "fallback-remote.json").read_text())
    payload = json.loads(str(fallback.get("response") or "{}"))
    assert payload.get("message") == "output-sanitized"
    assert calls == {"operator": 0, "output": 1}


def test_core_skips_superseded_inbox_task_when_newer_matching_task_exists(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured.append(str(user_input or ""))
        return {"message": "latest ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    (inbox / "old.json").write_text(
        json.dumps({"id": "old", "prompt": "status", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    (inbox / "new.json").write_text(
        json.dumps({"id": "new", "prompt": "status", "source": "cli", "timestamp": 2, "metadata": {"channel_id": "1"}})
    )

    handler._process_inbox_task(str(inbox / "old.json"))
    old_payload = json.loads((config_dir / "outbox" / "old.json").read_text())
    old_message = json.loads(old_payload["response"])["message"]
    assert "Skipped this request because a newer request from the same chat/session was submitted." in old_message
    assert "latest task `new`" in old_message
    assert captured == []

    handler._process_inbox_task(str(inbox / "new.json"))
    assert captured
    assert not (inbox / "old.json").exists()
    assert not (inbox / "new.json").exists()


@pytest.mark.parametrize("source_name", ["telegram", "signal"])
def test_core_does_not_skip_newer_task_from_different_messaging_user_in_same_channel(
    tmp_path: Path,
    monkeypatch,
    source_name: str,
):
    config_dir = _setup(tmp_path, monkeypatch)
    monkeypatch.setenv("ARMORER_REQUIRE_REMOTE_TASK_SIGNATURES", "false")
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured.append(str(user_input or ""))
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    old_task = inbox / "old.json"
    new_task = inbox / "new.json"
    old_task.write_text(
        json.dumps(
            {
                "id": "old",
                "prompt": "status",
                "source": source_name,
                "timestamp": 1,
                "metadata": {"channel_id": "-100group", "user_id": "111"},
            }
        )
    )
    new_task.write_text(
        json.dumps(
            {
                "id": "new",
                "prompt": "status",
                "source": source_name,
                "timestamp": 2,
                "metadata": {"channel_id": "-100group", "user_id": "222"},
            }
        )
    )

    handler._process_inbox_task(str(old_task))
    old_payload = json.loads((config_dir / "outbox" / "old.json").read_text())
    old_message = json.loads(old_payload["response"])["message"]
    assert old_message == "ok"
    assert captured == ["status"]

    handler._process_inbox_task(str(new_task))
    new_payload = json.loads((config_dir / "outbox" / "new.json").read_text())
    new_message = json.loads(new_payload["response"])["message"]
    assert new_message == "ok"
    assert captured == ["status", "status"]
    assert not old_task.exists()
    assert not new_task.exists()


def test_poll_inbox_does_not_strand_task_when_handler_is_busy(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    inbox = config_dir / "inbox"
    task_path = inbox / "queued.json"
    task_path.write_text(
        json.dumps(
            {
                "id": "queued",
                "prompt": "status",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    handler.is_processing = True
    handler._poll_inbox()

    assert not (config_dir / "outbox" / "queued.json").exists()

    handler.is_processing = False
    handler._poll_inbox()

    out = json.loads((config_dir / "outbox" / "queued.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("message") == "ok"


def test_poll_inbox_processes_recreated_task_path_after_retry(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    inbox = config_dir / "inbox"
    task_path = inbox / "retry.json"
    handler.last_processed[str(task_path)] = time.time() - 10
    task_path.write_text(
        json.dumps(
            {
                "id": "retry",
                "prompt": "status",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    handler._poll_inbox()

    out = json.loads((config_dir / "outbox" / "retry.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("message") == "ok"


def test_on_moved_processes_atomically_published_inbox_task(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    final_path = inbox / "moved.json"
    final_path.write_text(
        json.dumps(
            {
                "id": "moved",
                "prompt": "status",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    event = SimpleNamespace(is_directory=False, dest_path=str(final_path))
    handler.on_moved(event)

    out = json.loads((config_dir / "outbox" / "moved.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("message") == "ok"


def test_incomplete_inbox_json_is_left_for_retry_and_later_succeeds(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    task_path = inbox / "partial.json"
    task_path.write_text('{"id": "partial", "prompt": "status"')

    handler._process_inbox_task(str(task_path))

    assert task_path.exists()
    assert not (config_dir / "outbox" / "partial.json").exists()

    task_path.write_text(
        json.dumps(
            {
                "id": "partial",
                "prompt": "status",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    handler._poll_inbox()

    out = json.loads((config_dir / "outbox" / "partial.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("message") == "ok"


def test_telegram_setup_requests_token_and_skips_llm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    def _should_not_run(*args, **kwargs):
        raise AssertionError("LLM should not run for telegram setup preflight.")

    monkeypatch.setattr(core_mod.BaseAgent, "run", _should_not_run, raising=True)

    inbox = config_dir / "inbox"
    (inbox / "tg1.json").write_text(
        json.dumps(
            {
                "id": "tg1",
                "prompt": "Can you help me setup telegram?",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "tg1.json"))

    out = json.loads((config_dir / "outbox" / "tg1.json").read_text())
    payload = json.loads(out["response"])
    assert payload.get("ui_action", {}).get("type") == "text"
    assert "TELEGRAM_BOT_TOKEN" in payload.get("ui_action", {}).get("message", "")
    assert payload.get("ui_action", {}).get("sensitive") is True


def test_telegram_setup_flow_sets_config_and_returns_gateway_command(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"

    # 1) Ask setup -> token prompt
    (inbox / "tg1.json").write_text(
        json.dumps({"id": "tg1", "prompt": "setup telegram integration", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg1.json"))
    payload1 = json.loads(json.loads((config_dir / "outbox" / "tg1.json").read_text())["response"])
    assert payload1.get("ui_action", {}).get("type") == "text"
    assert payload1.get("ui_action", {}).get("sensitive") is True

    # 2) Provide token -> admins prompt
    (inbox / "tg2.json").write_text(
        json.dumps({"id": "tg2", "prompt": "UI input: 123456:abcDEF_token_sample_value", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg2.json"))
    payload2 = json.loads(json.loads((config_dir / "outbox" / "tg2.json").read_text())["response"])
    assert payload2.get("ui_action", {}).get("type") == "text"
    assert "admin" in payload2.get("message", "").lower()

    # 3) Provide admins -> alert chat prompt
    (inbox / "tg3.json").write_text(
        json.dumps({"id": "tg3", "prompt": "UI input: 12345,-100999", "source": "cli", "timestamp": 2, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg3.json"))
    payload3 = json.loads(json.loads((config_dir / "outbox" / "tg3.json").read_text())["response"])
    assert payload3.get("ui_action", {}).get("type") == "text"
    assert "ALERT_CHAT_ID" in payload3.get("message", "")

    # 4) Skip alert chat -> interactive command
    (inbox / "tg4.json").write_text(
        json.dumps({"id": "tg4", "prompt": "UI input: skip", "source": "cli", "timestamp": 3, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg4.json"))
    payload4 = json.loads(json.loads((config_dir / "outbox" / "tg4.json").read_text())["response"])
    assert payload4.get("ui_action", {}).get("type") == "interactive_command"
    assert payload4.get("ui_action", {}).get("command") == "armorer gateway telegram"

    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("TELEGRAM_BOT_TOKEN") == "123456:abcDEF_token_sample_value"
    assert cm.get_key("ARMORER_TELEGRAM_ADMIN_USER_IDS") == "12345,-100999"
    assert cm.get_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS") == "12345,-100999"
    assert cm.get_key("ARMORER_GATEWAY_TELEGRAM_ENABLED") == "true"


def test_telegram_setup_flow_auto_discovers_ids_from_updates(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    class _FakeGateway:
        def __init__(self, bot_token):
            self.bot_token = bot_token

        def get_updates(self, **_kwargs):
            return [
                {
                    "message": {
                        "chat": {"id": 900100200, "type": "private"},
                        "from": {"id": 900100200},
                    }
                }
            ]

    monkeypatch.setattr("armorer.core.telegram_setup.TelegramGateway", _FakeGateway)

    inbox = config_dir / "inbox"
    (inbox / "tg_auto_1.json").write_text(
        json.dumps(
            {
                "id": "tg_auto_1",
                "prompt": "setup telegram integration",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "tg_auto_1.json"))

    (inbox / "tg_auto_2.json").write_text(
        json.dumps(
            {
                "id": "tg_auto_2",
                "prompt": "UI input: 123456:abcDEF_token_sample_value",
                "source": "cli",
                "timestamp": 1,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "tg_auto_2.json"))
    payload = json.loads(json.loads((config_dir / "outbox" / "tg_auto_2.json").read_text())["response"])

    assert payload.get("ui_action", {}).get("type") == "interactive_command"
    assert "auto-configured Telegram admin/access settings" in payload.get("message", "")

    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("ARMORER_TELEGRAM_ADMIN_USER_IDS") == "900100200"
    assert cm.get_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS") == "900100200"
    assert cm.get_key("TELEGRAM_ALERT_CHAT_ID") == "900100200"
    assert cm.get_key("ARMORER_GATEWAY_TELEGRAM_ENABLED") == "true"


def test_telegram_setup_experimental_mode_prompts_for_choice(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    cm = ConfigManager()
    cm.use_keyring = False
    cm.set_key("ARMORER_TELEGRAM_EXPERIMENTAL_AUTOCREATE", "true")

    inbox = config_dir / "inbox"
    (inbox / "tg_exp_1.json").write_text(
        json.dumps(
            {
                "id": "tg_exp_1",
                "prompt": "setup telegram integration",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "tg_exp_1.json"))
    payload = json.loads(json.loads((config_dir / "outbox" / "tg_exp_1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "confirm"
    assert "experimental telegram bot creation" in payload.get("message", "").lower()


def test_telegram_setup_experimental_mode_falls_back_to_token_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    cm = ConfigManager()
    cm.use_keyring = False
    cm.set_key("ARMORER_TELEGRAM_EXPERIMENTAL_AUTOCREATE", "true")

    monkeypatch.setattr(
        "armorer.core.telegram_experimental_provisioning.attempt_bot_creation",
        lambda _cfg: {"status": "failed", "reason": "simulated experimental failure"},
    )

    inbox = config_dir / "inbox"
    (inbox / "tg_exp_1.json").write_text(
        json.dumps(
            {
                "id": "tg_exp_1",
                "prompt": "setup telegram integration",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "tg_exp_1.json"))

    (inbox / "tg_exp_2.json").write_text(
        json.dumps(
            {
                "id": "tg_exp_2",
                "prompt": "UI confirmation: yes",
                "source": "cli",
                "timestamp": 1,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "tg_exp_2.json"))
    payload = json.loads(json.loads((config_dir / "outbox" / "tg_exp_2.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "text"
    assert "simulated experimental failure" in payload.get("message", "")
    assert "Paste TELEGRAM_BOT_TOKEN" in payload.get("ui_action", {}).get("message", "")


def test_pending_telegram_setup_does_not_hijack_unrelated_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "normal_flow_ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"

    # Start Telegram setup so pending state exists.
    (inbox / "tg1.json").write_text(
        json.dumps({"id": "tg1", "prompt": "setup telegram", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg1.json"))

    # Unrelated prompt should not be consumed by Telegram flow.
    (inbox / "q1.json").write_text(
        json.dumps({"id": "q1", "prompt": "what agents are installed right now?", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "q1.json"))

    assert "what agents are installed right now?" in captured.get("user_input", "")
    payload = json.loads(json.loads((config_dir / "outbox" / "q1.json").read_text())["response"])
    assert payload.get("message") == "normal_flow_ok"


def test_core_prompt_context_includes_openclaw_fallback_when_registry_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_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"
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _fake_init_agent(self, *args, **kwargs):
        captured["system_prompt"] = str(kwargs.get("system_prompt") or "")
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _fake_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    task_path = inbox / "ctx1.json"
    task_path.write_text(
        json.dumps(
            {
                "id": "ctx1",
                "prompt": "what agents are installed right now?",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )

    handler._process_inbox_task(str(task_path))

    payload = json.loads(json.loads((config_dir / "outbox" / "ctx1.json").read_text())["response"])
    assert payload.get("message") == "ok"
    assert captured.get("user_input") == "what agents are installed right now?"
    assert "Registered Applications:" in captured.get("system_prompt", "")
    assert str(install_dir) in captured.get("system_prompt", "")
    assert "- openclaw at " in captured.get("system_prompt", "")


def test_telegram_setup_admin_step_answers_question_instead_of_advancing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"

    # Enter admin collection step.
    (inbox / "tg1.json").write_text(
        json.dumps({"id": "tg1", "prompt": "setup telegram integration", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg1.json"))
    (inbox / "tg2.json").write_text(
        json.dumps({"id": "tg2", "prompt": "UI input: 123456:abcDEF_token_sample_value", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg2.json"))

    # Ask a question instead of giving IDs.
    (inbox / "tg3.json").write_text(
        json.dumps({"id": "tg3", "prompt": "UI input: how can I get that?", "source": "cli", "timestamp": 2, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg3.json"))
    payload3 = json.loads(json.loads((config_dir / "outbox" / "tg3.json").read_text())["response"])

    assert payload3.get("ui_action", {}).get("type") == "text"
    assert "getUpdates" in payload3.get("message", "")
    assert "from.id" in payload3.get("message", "")
    assert "chat.id" in payload3.get("message", "")


def test_telegram_setup_admin_step_rejects_invalid_ids(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"

    # Enter admin collection step.
    (inbox / "tg1.json").write_text(
        json.dumps({"id": "tg1", "prompt": "setup telegram integration", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg1.json"))
    (inbox / "tg2.json").write_text(
        json.dumps({"id": "tg2", "prompt": "UI input: 123456:abcDEF_token_sample_value", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg2.json"))

    # Invalid IDs should not advance flow.
    (inbox / "tg3.json").write_text(
        json.dumps({"id": "tg3", "prompt": "UI input: abc,-hello", "source": "cli", "timestamp": 2, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg3.json"))
    payload3 = json.loads(json.loads((config_dir / "outbox" / "tg3.json").read_text())["response"])

    assert payload3.get("ui_action", {}).get("type") == "text"
    assert "numeric Telegram user/chat IDs" in payload3.get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "telegram_setup_admins"


def test_telegram_setup_alert_chat_rejects_invalid_chat_id(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    cm = ConfigManager()
    cm.use_keyring = False
    cm.set_key("TELEGRAM_BOT_TOKEN", "123456:abcDEF_token_sample_value")
    cm.set_key("ARMORER_TELEGRAM_ADMIN_USER_IDS", "12345")
    cm.set_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS", "12345")

    inbox = config_dir / "inbox"
    (inbox / "tg1.json").write_text(
        json.dumps({"id": "tg1", "prompt": "setup telegram integration", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg1.json"))

    (inbox / "tg2.json").write_text(
        json.dumps({"id": "tg2", "prompt": "UI input: not-a-chat-id", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg2.json"))
    payload2 = json.loads(json.loads((config_dir / "outbox" / "tg2.json").read_text())["response"])

    assert payload2.get("ui_action", {}).get("type") == "text"
    assert "TELEGRAM_ALERT_CHAT_ID must be a numeric Telegram chat ID" in payload2.get("message", "")


def test_telegram_setup_alert_chat_rejects_blank_without_explicit_skip(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    cm = ConfigManager()
    cm.use_keyring = False
    cm.set_key("TELEGRAM_BOT_TOKEN", "123456:abcDEF_token_sample_value")
    cm.set_key("ARMORER_TELEGRAM_ADMIN_USER_IDS", "12345")
    cm.set_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS", "12345")

    inbox = config_dir / "inbox"
    (inbox / "tg_blank_1.json").write_text(
        json.dumps({"id": "tg_blank_1", "prompt": "setup telegram integration", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg_blank_1.json"))

    (inbox / "tg_blank_2.json").write_text(
        json.dumps({"id": "tg_blank_2", "prompt": "UI input: ", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg_blank_2.json"))
    payload2 = json.loads(json.loads((config_dir / "outbox" / "tg_blank_2.json").read_text())["response"])

    assert payload2.get("ui_action", {}).get("type") == "text"
    assert "Enter TELEGRAM_ALERT_CHAT_ID or reply `skip`" in payload2.get("message", "")


def test_pending_telegram_admin_step_routes_unrelated_question_to_llm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"

    # Enter admin collection step.
    (inbox / "tg1.json").write_text(
        json.dumps({"id": "tg1", "prompt": "setup telegram integration", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg1.json"))
    (inbox / "tg2.json").write_text(
        json.dumps({"id": "tg2", "prompt": "UI input: 123456:abcDEF_token_sample_value", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "tg2.json"))

    # Unrelated question should be sent to normal LLM path, not consumed as admin IDs.
    (inbox / "q1.json").write_text(
        json.dumps({"id": "q1", "prompt": "UI input: what agents are currently running?", "source": "cli", "timestamp": 2, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "q1.json"))
    payload = json.loads(json.loads((config_dir / "outbox" / "q1.json").read_text())["response"])

    assert payload.get("message") == "llm_answer"
    assert "what agents are currently running?" in captured.get("user_input", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "telegram_setup_admins"


def test_setup_security_profile_is_requested_for_third_party_agent_setup(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "s1.json").write_text(
        json.dumps(
            {
                "id": "s1",
                "prompt": "install https://github.com/Significant-Gravitas/AutoGPT and configure it",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "s1.json"))
    payload = json.loads(json.loads((config_dir / "outbox" / "s1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("options") == ["strict", "balanced", "flexible"]


def test_setup_security_profile_selection_resumes_with_context(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    (inbox / "s1.json").write_text(
        json.dumps(
            {
                "id": "s1",
                "prompt": "setup third party assistant from github.com/foo/bar",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "s1.json"))

    (inbox / "s2.json").write_text(
        json.dumps(
            {
                "id": "s2",
                "prompt": "UI selection: strict",
                "source": "cli",
                "timestamp": 1,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "s2.json"))
    payload = json.loads(json.loads((config_dir / "outbox" / "s2.json").read_text())["response"])
    assert payload.get("message") == "ok"
    assert "[SECURE_SETUP_PROFILE=strict]" in captured.get("user_input", "")


def test_focus_mode_blocks_unrelated_questions_during_setup_config(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "f1.json").write_text(
        json.dumps({"id": "f1", "prompt": "help me setup telegram for openclaw", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "f1.json"))

    (inbox / "f2.json").write_text(
        json.dumps({"id": "f2", "prompt": "what is the weather today?", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "f2.json"))
    payload = json.loads(json.loads((config_dir / "outbox" / "f2.json").read_text())["response"])

    assert payload.get("ui_action", {}).get("type") == "confirm"
    assert "focused on assistant setup/configuration" in payload.get("message", "").lower()


def test_focus_mode_switch_yes_resumes_unrelated_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "switched_ok"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    (inbox / "f1.json").write_text(
        json.dumps({"id": "f1", "prompt": "setup telegram for autogpt", "source": "cli", "timestamp": 0, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "f1.json"))

    (inbox / "f2.json").write_text(
        json.dumps({"id": "f2", "prompt": "tell me a joke", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "f2.json"))

    (inbox / "f3.json").write_text(
        json.dumps({"id": "f3", "prompt": "UI confirmation: yes", "source": "cli", "timestamp": 2, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "f3.json"))
    payload = json.loads(json.loads((config_dir / "outbox" / "f3.json").read_text())["response"])

    assert payload.get("message") == "switched_ok"
    assert "tell me a joke" in captured.get("user_input", "")


def test_openclaw_setup_flow_runs_end_to_end_in_chat(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_USER_IDS", "9001")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS", "-1009001")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS", "false")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")

    status_calls: list[str] = []
    config_values: dict[str, object] = {}
    skill_registry = {
        "filesystem": SimpleNamespace(
            name="filesystem",
            config={"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/Shared"]},
        )
    }

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return list(skill_registry.values())

        def get_skill(self, name: str):
            return skill_registry.get(name)

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            status_calls.append("status")
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            status_calls.append("run-background")
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                if isinstance(value, list):
                    out = json.dumps(value)
                else:
                    out = str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw for me"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc6", "UI input: 12345,-100999"),
        ("oc7", "UI input: -100777"),
        ("oc8", "UI input: filesystem"),
        ("oc9", "UI input: skills,.openclaw/skills"),
        ("oc10", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Telegram channel configuration verified via Telegram API" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")
    assert "Injected Armory skills: filesystem" in last_payload.get("message", "")
    assert status_calls == ["run-background", "run-background", "status"]
    assert config_values["channels.telegram.botToken"] == "123456:abcDEF_token_sample_value"
    assert config_values["channels.telegram.enabled"] == "true"
    assert config_values["channels.telegram.groupPolicy"] == "allowlist"
    assert config_values["channels.telegram.allowFrom"] == ["12345", "-100999"]
    assert config_values["channels.telegram.groupAllowFrom"] == ["12345", "-100999"]
    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "openai/gpt-4.1-mini"
    assert cfg["mcpServers"]["filesystem"]["command"] == "npx"
    assert cfg["skills"]["load"]["extraDirs"] == ["skills", ".openclaw/skills"]
    root_env = (compose_dir / ".env").read_text()
    assert "ARMORER_SKILL_SCAN_PATHS=skills .openclaw/skills" in root_env
    openclaw_env = (compose_dir / ".openclaw" / ".env").read_text()
    assert "OPENAI_API_KEY=sk-test-openai" in openclaw_env
    assert "TELEGRAM_ALERT_CHAT_ID=-100777" in openclaw_env
    assert "ARMORER_SKILL_SCAN_PATHS=skills .openclaw/skills" in openclaw_env
    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("MODEL_PROVIDER") == "openai"
    assert cm.get_key("Armorer_MODEL") == "gpt-4.1-mini"
    assert cm.get_key("OPENAI_API_KEY") == "sk-test-openai"
    assert cm.get_key("ARMORER_TELEGRAM_ADMIN_USER_IDS") == "9001"
    assert cm.get_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS") == "-1009001"
    assert cm.get_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS") == "false"
    assert cm.get_key("TELEGRAM_ALERT_CHAT_ID") == "-100777"
    assert cm.get_key("ARMORER_SKILL_SCAN_PATHS") == "skills .openclaw/skills"
    app_meta = registry.get_app("openclaw")
    assert app_meta is not None
    assert "filesystem" in app_meta.injected_skills


def test_openclaw_setup_flow_succeeds_when_armorer_config_mirror_save_fails(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                values = {
                    "channels.telegram.botToken": "",
                    "channels.telegram.enabled": "false",
                    "channels.telegram.groupPolicy": "open",
                    "channels.telegram.allowFrom": "[]",
                    "channels.telegram.groupAllowFrom": "[]",
                }
                return subprocess.CompletedProcess(args, 0, stdout=values.get(key, ""), stderr="")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(subprocess, "run", _fake_run)
    monkeypatch.setattr(
        handler.config_manager,
        "set_key",
        lambda key, value: (_ for _ in ()).throw(PermissionError("read-only armorer config")),
    )

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw for me"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Warning: Armorer local config mirror could not be updated for:" in last_payload.get("message", "")
    assert "Final status check passed via `armorer run openclaw status`." in last_payload.get("message", "")
    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "openai/gpt-4.1-mini"
    openclaw_env = (compose_dir / ".openclaw" / ".env").read_text()
    assert "OPENAI_API_KEY=sk-test-openai" in openclaw_env


def test_execute_openclaw_setup_reports_alias_specific_status_command(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "installed" / "prindleclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    runtime = {
        "provider": "openai",
        "model": "gpt-4.1-mini",
        "model_ref": "openai/gpt-4.1-mini",
        "provider_key_name": "OPENAI_API_KEY",
        "provider_key_value": "sk-test-openai",
        "base_url": "",
        "skill_paths": [],
        "env_updates": {},
        "alert_chat_id": "",
    }
    run_calls: list[list[str]] = []

    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_settings", lambda state: dict(runtime))
    monkeypatch.setattr(handler, "_assert_openclaw_runtime_prerequisites", lambda runtime_settings: None)
    monkeypatch.setattr(handler, "_write_task_status", lambda task_id, message: None)
    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_compose_dir", lambda app_name: compose_dir)
    monkeypatch.setattr(
        handler,
        "_recover_openclaw_registry_entry",
        lambda app_name, task_id: AppMetadata(name=app_name, install_path=str(compose_dir), agent_id="openclaw"),
    )
    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda cmd, **kwargs: run_calls.append([str(x) for x in cmd]) or subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr=""),
    )
    monkeypatch.setattr(handler, "_openclaw_registry_entry_matches_runtime", lambda app_name, runtime_dir: True)
    monkeypatch.setattr(handler, "_sync_openclaw_runtime_settings", lambda runtime_dir, state: dict(runtime))
    monkeypatch.setattr(handler, "_sync_openclaw_armory_skills", lambda *args, **kwargs: [])
    monkeypatch.setattr(handler, "_verify_openclaw_setup", lambda app_name, state, task_id: "OpenClaw status: healthy")
    monkeypatch.setattr(
        handler,
        "_verify_openclaw_telegram_channel",
        lambda app_name, state, runtime_settings, task_id: {"bot_username": "", "verified_chat_ids": []},
    )
    monkeypatch.setattr(handler.config_manager, "set_key", lambda key, value: None)

    payload = handler._execute_openclaw_setup(
        {
            "alias": "prindleclaw",
            "provider": "openai",
            "model": "gpt-4.1-mini",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "sk-test-openai",
        },
        "oc-alias",
    )

    assert "Final status check passed via `armorer run prindleclaw status`." in payload.get("message", "")
    assert run_calls == [
        ["run", "prindleclaw", "--mode", "background"],
        ["run", "prindleclaw", "--mode", "background"],
    ]


def test_execute_openclaw_setup_alias_registry_warning_mentions_alias_command(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "installed" / "prindleclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    runtime = {
        "provider": "openai",
        "model": "gpt-4.1-mini",
        "model_ref": "openai/gpt-4.1-mini",
        "provider_key_name": "OPENAI_API_KEY",
        "provider_key_value": "sk-test-openai",
        "base_url": "",
        "skill_paths": [],
        "env_updates": {},
        "alert_chat_id": "",
    }

    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_settings", lambda state: dict(runtime))
    monkeypatch.setattr(handler, "_assert_openclaw_runtime_prerequisites", lambda runtime_settings: None)
    monkeypatch.setattr(handler, "_write_task_status", lambda task_id, message: None)
    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_compose_dir", lambda app_name: compose_dir)
    monkeypatch.setattr(
        handler,
        "_recover_openclaw_registry_entry",
        lambda app_name, task_id: AppMetadata(name=app_name, install_path=str(compose_dir), agent_id="openclaw"),
    )
    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr=""),
    )
    monkeypatch.setattr(handler, "_openclaw_registry_entry_matches_runtime", lambda app_name, runtime_dir: False)
    monkeypatch.setattr(handler, "_sync_openclaw_runtime_settings", lambda runtime_dir, state: dict(runtime))
    monkeypatch.setattr(handler, "_sync_openclaw_armory_skills", lambda *args, **kwargs: [])
    monkeypatch.setattr(handler, "_verify_openclaw_setup", lambda app_name, state, task_id: "OpenClaw status: healthy")
    monkeypatch.setattr(
        handler,
        "_verify_openclaw_telegram_channel",
        lambda app_name, state, runtime_settings, task_id: {"bot_username": "", "verified_chat_ids": []},
    )
    monkeypatch.setattr(handler.config_manager, "set_key", lambda key, value: None)

    payload = handler._execute_openclaw_setup(
        {
            "alias": "prindleclaw",
            "provider": "openai",
            "model": "gpt-4.1-mini",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "sk-test-openai",
        },
        "oc-alias-warning",
    )

    message = payload.get("message", "")
    assert "Warning: Armorer could not persist OpenClaw registry metadata for `prindleclaw`." in message
    assert "future `armorer run prindleclaw ...` commands" in message
    assert "`armorer run openclaw ...`" not in message


def test_execute_openclaw_setup_reinstalls_when_registry_points_to_expected_path_without_runtime(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    monkeypatch.chdir(tmp_path)

    install_root = tmp_path / "installed" / "openclaw"
    install_root.mkdir(parents=True, exist_ok=True)
    (install_root / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )

    runtime = {
        "provider": "openai",
        "model": "gpt-4.1-mini",
        "model_ref": "openai/gpt-4.1-mini",
        "provider_key_name": "OPENAI_API_KEY",
        "provider_key_value": "sk-test-openai",
        "base_url": "",
        "skill_paths": [],
        "env_updates": {},
        "alert_chat_id": "",
    }
    run_calls: list[list[str]] = []
    resolve_calls = {"count": 0}

    registry_app = AppMetadata(name="openclaw", install_path=str(install_root), agent_id="openclaw")
    monkeypatch.setattr(handler.registry, "get_app", lambda app_name: registry_app)

    def _resolve_runtime_dir(_app_name: str):
        resolve_calls["count"] += 1
        if resolve_calls["count"] == 1:
            return None
        return install_root

    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_compose_dir", _resolve_runtime_dir)
    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_settings", lambda state: dict(runtime))
    monkeypatch.setattr(handler, "_resolved_openclaw_armory_skill_names", lambda state, app_name: [])
    monkeypatch.setattr(handler, "_assert_openclaw_runtime_prerequisites", lambda runtime_settings: None)
    monkeypatch.setattr(handler, "_write_task_status", lambda task_id, message: None)
    monkeypatch.setattr(
        handler,
        "_recover_openclaw_registry_entry",
        lambda app_name, task_id: AppMetadata(name=app_name, install_path=str(install_root), agent_id="openclaw"),
    )
    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda cmd, **kwargs: run_calls.append([str(x) for x in cmd]) or subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr=""),
    )
    monkeypatch.setattr(handler, "_openclaw_registry_entry_matches_runtime", lambda app_name, runtime_dir: True)
    monkeypatch.setattr(handler, "_sync_openclaw_runtime_settings", lambda runtime_dir, state: dict(runtime))
    monkeypatch.setattr(handler, "_sync_openclaw_armory_skills", lambda *args, **kwargs: [])
    monkeypatch.setattr(handler, "_verify_openclaw_setup", lambda app_name, state, task_id: "OpenClaw status: healthy")
    monkeypatch.setattr(
        handler,
        "_verify_openclaw_telegram_channel",
        lambda app_name, state, runtime_settings, task_id: {"bot_username": "", "verified_chat_ids": []},
    )
    monkeypatch.setattr(handler.config_manager, "set_key", lambda key, value: None)

    payload = handler._execute_openclaw_setup(
        {
            "alias": "openclaw",
            "provider": "openai",
            "model": "gpt-4.1-mini",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "sk-test-openai",
        },
        "oc-reinstall-missing-runtime",
    )

    assert "OpenClaw is installed and verified as `openclaw`." in payload.get("message", "")
    assert run_calls[:3] == [
        ["install", "openclaw", "--yes", "--skip-scan", "--alias", "openclaw"],
        ["run", "openclaw", "--mode", "background"],
        ["run", "openclaw", "--mode", "background"],
    ]


def test_openclaw_rollback_summary_for_snapshot_reports_registry_only_partial_setup(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    current_registry_app = AppMetadata(
        name="openclaw",
        install_path=str(tmp_path / "installed" / "openclaw"),
        agent_id="openclaw",
    )
    monkeypatch.setattr(handler.registry, "get_app", lambda app_name: current_registry_app)

    snapshot = {
        "app_name": "openclaw",
        "had_runtime": False,
        "install_roots": {},
        "registry_app": None,
    }

    summary = handler._openclaw_rollback_summary_for_snapshot(snapshot)

    assert summary == "Armorer rolled back the partial OpenClaw setup state before retry."


def test_execute_openclaw_setup_rolls_back_runtime_files_after_late_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)

    baseline_env = "OPENAI_API_KEY=sk-old-openai\n"
    baseline_openclaw_env = "OPENAI_API_KEY=sk-old-openai\n"
    baseline_json = {
        "agents": {"defaults": {"model": {"primary": "openai/old-model"}}},
        "mcpServers": {
            "existing-skill": {"command": "npx", "args": ["-y", "existing-skill"]},
        },
    }
    (compose_dir / ".env").write_text(baseline_env)
    (cfg_dir / ".env").write_text(baseline_openclaw_env)
    (cfg_dir / "openclaw.json").write_text(json.dumps(baseline_json, indent=2) + "\n")

    handler.registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            injected_skills=["existing-skill"],
        )
    )

    runtime = {
        "provider": "openai",
        "model": "new-model",
        "model_ref": "openai/new-model",
        "provider_key_name": "OPENAI_API_KEY",
        "provider_key_value": "sk-new-openai",
        "base_url": "",
        "skill_paths": ["skills", ".openclaw/skills"],
        "env_updates": {
            "OPENAI_API_KEY": "sk-new-openai",
            "ARMORER_SKILL_SCAN_PATHS": "skills .openclaw/skills",
        },
        "alert_chat_id": "",
    }

    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_settings", lambda state: dict(runtime))
    monkeypatch.setattr(handler, "_assert_openclaw_runtime_prerequisites", lambda runtime_settings: None)
    monkeypatch.setattr(handler, "_write_task_status", lambda task_id, message: None)
    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_compose_dir", lambda app_name: compose_dir)
    monkeypatch.setattr(
        handler,
        "_recover_openclaw_registry_entry",
        lambda app_name, task_id: handler.registry.get_app(app_name),
    )
    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr=""),
    )
    monkeypatch.setattr(handler, "_openclaw_registry_entry_matches_runtime", lambda app_name, runtime_dir: True)

    def _mutating_sync(runtime_dir: Path, state: dict) -> dict[str, object]:
        (runtime_dir / ".env").write_text("OPENAI_API_KEY=sk-new-openai\nARMORER_SKILL_SCAN_PATHS=skills .openclaw/skills\n")
        (runtime_dir / ".openclaw" / ".env").write_text(
            "OPENAI_API_KEY=sk-new-openai\nARMORER_SKILL_SCAN_PATHS=skills .openclaw/skills\n"
        )
        data = json.loads((runtime_dir / ".openclaw" / "openclaw.json").read_text())
        data["agents"]["defaults"]["model"]["primary"] = "openai/new-model"
        data["skills"] = {"load": {"extraDirs": ["skills", ".openclaw/skills"]}}
        (runtime_dir / ".openclaw" / "openclaw.json").write_text(json.dumps(data, indent=2) + "\n")
        return dict(runtime)

    def _mutating_skills(runtime_dir: Path, app_name: str, skill_names: list[str], **kwargs) -> list[str]:
        data = json.loads((runtime_dir / ".openclaw" / "openclaw.json").read_text())
        data["mcpServers"] = {
            "filesystem": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/Shared"]}
        }
        (runtime_dir / ".openclaw" / "openclaw.json").write_text(json.dumps(data, indent=2) + "\n")
        app_meta = handler.registry.get_app(app_name)
        assert app_meta is not None
        app_meta.injected_skills = ["filesystem"]
        handler.registry.add_app(app_meta)
        return ["filesystem"]

    monkeypatch.setattr(handler, "_sync_openclaw_runtime_settings", _mutating_sync)
    monkeypatch.setattr(handler, "_sync_openclaw_armory_skills", _mutating_skills)
    monkeypatch.setattr(
        handler,
        "_verify_openclaw_setup",
        lambda app_name, state, task_id: (_ for _ in ()).throw(RuntimeError("verification failed")),
    )
    monkeypatch.setattr(
        handler,
        "_verify_openclaw_telegram_channel",
        lambda app_name, state, runtime_settings, task_id: {"bot_username": "", "verified_chat_ids": []},
    )
    monkeypatch.setattr(handler.config_manager, "set_key", lambda key, value: None)

    with pytest.raises(RuntimeError, match="verification failed"):
        handler._execute_openclaw_setup(
            {
                "alias": "openclaw",
                "provider": "openai",
                "model": "new-model",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-new-openai",
                "armory_skills": "filesystem",
                "skills_paths": "skills,.openclaw/skills",
            },
            "oc-rollback",
        )

    assert (compose_dir / ".env").read_text() == baseline_env
    assert (cfg_dir / ".env").read_text() == baseline_openclaw_env
    assert json.loads((cfg_dir / "openclaw.json").read_text()) == baseline_json
    restored_app = handler.registry.get_app("openclaw")
    assert restored_app is not None
    assert restored_app.injected_skills == ["existing-skill"]


def test_execute_openclaw_setup_reports_nonzero_reload_exit_as_rollback_warning(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)

    baseline_env = "OPENAI_API_KEY=sk-old-openai\n"
    baseline_openclaw_env = "OPENAI_API_KEY=sk-old-openai\n"
    baseline_json = {
        "agents": {"defaults": {"model": {"primary": "openai/old-model"}}},
        "mcpServers": {
            "existing-skill": {"command": "npx", "args": ["-y", "existing-skill"]},
        },
    }
    (compose_dir / ".env").write_text(baseline_env)
    (cfg_dir / ".env").write_text(baseline_openclaw_env)
    (cfg_dir / "openclaw.json").write_text(json.dumps(baseline_json, indent=2) + "\n")

    handler.registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            injected_skills=["existing-skill"],
        )
    )

    runtime = {
        "provider": "openai",
        "model": "new-model",
        "model_ref": "openai/new-model",
        "provider_key_name": "OPENAI_API_KEY",
        "provider_key_value": "sk-new-openai",
        "base_url": "",
        "skill_paths": ["skills", ".openclaw/skills"],
        "env_updates": {
            "OPENAI_API_KEY": "sk-new-openai",
            "ARMORER_SKILL_SCAN_PATHS": "skills .openclaw/skills",
        },
        "alert_chat_id": "",
    }

    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_settings", lambda state: dict(runtime))
    monkeypatch.setattr(handler, "_assert_openclaw_runtime_prerequisites", lambda runtime_settings: None)
    monkeypatch.setattr(handler, "_write_task_status", lambda task_id, message: None)
    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_compose_dir", lambda app_name: compose_dir)
    monkeypatch.setattr(
        handler,
        "_recover_openclaw_registry_entry",
        lambda app_name, task_id: handler.registry.get_app(app_name),
    )
    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda cmd, **kwargs: subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr=""),
    )
    monkeypatch.setattr(handler, "_openclaw_registry_entry_matches_runtime", lambda app_name, runtime_dir: True)

    def _mutating_sync(runtime_dir: Path, state: dict) -> dict[str, object]:
        (runtime_dir / ".env").write_text("OPENAI_API_KEY=sk-new-openai\nARMORER_SKILL_SCAN_PATHS=skills .openclaw/skills\n")
        (runtime_dir / ".openclaw" / ".env").write_text(
            "OPENAI_API_KEY=sk-new-openai\nARMORER_SKILL_SCAN_PATHS=skills .openclaw/skills\n"
        )
        data = json.loads((runtime_dir / ".openclaw" / "openclaw.json").read_text())
        data["agents"]["defaults"]["model"]["primary"] = "openai/new-model"
        data["skills"] = {"load": {"extraDirs": ["skills", ".openclaw/skills"]}}
        (runtime_dir / ".openclaw" / "openclaw.json").write_text(json.dumps(data, indent=2) + "\n")
        return dict(runtime)

    def _mutating_skills(runtime_dir: Path, app_name: str, skill_names: list[str], **kwargs) -> list[str]:
        data = json.loads((runtime_dir / ".openclaw" / "openclaw.json").read_text())
        data["mcpServers"] = {
            "filesystem": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/Shared"]}
        }
        (runtime_dir / ".openclaw" / "openclaw.json").write_text(json.dumps(data, indent=2) + "\n")
        app_meta = handler.registry.get_app(app_name)
        assert app_meta is not None
        app_meta.injected_skills = ["filesystem"]
        handler.registry.add_app(app_meta)
        return ["filesystem"]

    monkeypatch.setattr(handler, "_sync_openclaw_runtime_settings", _mutating_sync)
    monkeypatch.setattr(handler, "_sync_openclaw_armory_skills", _mutating_skills)
    monkeypatch.setattr(
        handler,
        "_verify_openclaw_setup",
        lambda app_name, state, task_id: (_ for _ in ()).throw(RuntimeError("verification failed")),
    )
    monkeypatch.setattr(
        handler,
        "_verify_openclaw_telegram_channel",
        lambda app_name, state, runtime_settings, task_id: {"bot_username": "", "verified_chat_ids": []},
    )
    monkeypatch.setattr(handler.config_manager, "set_key", lambda key, value: None)

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"

    def _fake_reload(cmd, **kwargs):
        return subprocess.CompletedProcess(cmd, 17, stdout="", stderr=f"docker unavailable {leaked_key}")

    monkeypatch.setattr("armorer.core.subprocess.run", _fake_reload)

    with pytest.raises(OpenClawSetupExecutionError) as exc_info:
        handler._execute_openclaw_setup(
            {
                "alias": "openclaw",
                "provider": "openai",
                "model": "new-model",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-new-openai",
                "armory_skills": "filesystem",
                "skills_paths": "skills,.openclaw/skills",
            },
            "oc-rollback",
        )

    assert exc_info.value.rollback_warnings == [
        "reload services: exit code 17 (docker unavailable [REDACTED_OPENAI_KEY])"
    ]
    assert leaked_key not in "\n".join(exc_info.value.rollback_warnings)
    assert (compose_dir / ".env").read_text() == baseline_env
    assert (cfg_dir / ".env").read_text() == baseline_openclaw_env
    assert json.loads((cfg_dir / "openclaw.json").read_text()) == baseline_json
    restored_app = handler.registry.get_app("openclaw")
    assert restored_app is not None
    assert restored_app.injected_skills == ["existing-skill"]


def test_execute_openclaw_setup_rolls_back_fresh_install_after_start_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    compose_dir = tmp_path / "installed" / "openclaw"

    runtime = {
        "provider": "openai",
        "model": "new-model",
        "model_ref": "openai/new-model",
        "provider_key_name": "OPENAI_API_KEY",
        "provider_key_value": "sk-new-openai",
        "base_url": "",
        "skill_paths": [],
        "env_updates": {
            "OPENAI_API_KEY": "sk-new-openai",
        },
        "alert_chat_id": "",
    }

    monkeypatch.setattr(handler, "_resolve_openclaw_runtime_settings", lambda state: dict(runtime))
    monkeypatch.setattr(handler, "_assert_openclaw_runtime_prerequisites", lambda runtime_settings: None)
    monkeypatch.setattr(handler, "_write_task_status", lambda task_id, message: None)

    start_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            compose_dir.mkdir(parents=True, exist_ok=True)
            (compose_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )
            (compose_dir / ".openclaw").mkdir(parents=True, exist_ok=True)
            (compose_dir / ".openclaw" / "openclaw.json").write_text("{}\n")
            handler.registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            start_attempts["count"] += 1
            return subprocess.CompletedProcess(args, 1, stdout="", stderr="startup exploded")
        raise AssertionError(f"Unexpected command: {args}")

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

    with pytest.raises(OpenClawSetupExecutionError) as exc_info:
        handler._execute_openclaw_setup(
            {
                "alias": "openclaw",
                "provider": "openai",
                "model": "new-model",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-new-openai",
            },
            "oc-start-fail",
        )

    assert start_attempts["count"] == 2
    assert str(exc_info.value) == "Starting OpenClaw services failed: startup exploded"
    assert exc_info.value.rollback_warnings == []
    assert not compose_dir.exists()
    assert handler.registry.get_app("openclaw") is None


def test_openclaw_chat_setup_leaves_instance_ready_for_separate_run_status(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    status_calls: list[str] = []
    config_values: dict[str, object] = {}

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    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",
    )

    monkeypatch.setattr("armorer.main.resolve_playbook", lambda *args, **kwargs: known_spec)
    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._check_notifications", lambda: None)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, stdin=None, stdout=None, stderr=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            status_calls.append("setup-status")
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            status_calls.append("run-background")
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                out = json.dumps(value) if isinstance(value, list) else str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        if args == ["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]:
            status_calls.append("direct-status")
            return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw for me"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc6", "UI input: 12345,-100999"),
        ("oc7", "UI input: -100777"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI input: skip"),
        ("oc10", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

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

    assert result.exit_code == 0
    assert status_calls == ["run-background", "run-background", "setup-status", "direct-status"]


def test_openclaw_setup_flow_reuses_existing_registered_instance_without_reinstall(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (compose_dir / ".openclaw").mkdir(parents=True, exist_ok=True)
    (compose_dir / ".openclaw" / "openclaw.json").write_text(
        json.dumps(
            {
                "channels": {"telegram": {"enabled": False}},
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
            },
            indent=2,
        )
    )
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            raise AssertionError("existing registered instance should be reused without reinstall")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Reused the existing OpenClaw install for this setup run." in last_payload.get("message", "")
    assert [args[:4] for args in run_calls] == [
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_reinstalls_when_registered_runtime_is_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(tmp_path / "missing" / "openclaw"),
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            compose_dir.mkdir(parents=True, exist_ok=True)
            (compose_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Reused the existing OpenClaw install" not in last_payload.get("message", "")
    persisted = registry.get_app("openclaw")
    assert persisted is not None
    assert persisted.install_path == str(compose_dir)
    assert [args[:4] for args in run_calls] == [
        ["armorer", "install", "openclaw", "--yes"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_reinstalls_when_registered_directory_exists_but_is_not_runtime(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    stale_dir = tmp_path / "stale" / "openclaw"
    stale_dir.mkdir(parents=True, exist_ok=True)
    compose_dir = tmp_path / "installed" / "openclaw"
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(stale_dir),
            agent_id="openclaw",
        )
    )

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            compose_dir.mkdir(parents=True, exist_ok=True)
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    persisted = registry.get_app("openclaw")
    assert persisted is not None
    assert persisted.install_path == str(compose_dir)
    assert [args[:4] for args in run_calls] == [
        ["armorer", "install", "openclaw", "--yes"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_warns_when_stale_registry_entry_cannot_be_repaired(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(tmp_path / "missing" / "openclaw"),
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(
        handler.registry,
        "add_app",
        lambda app: (_ for _ in ()).throw(PermissionError("read-only registry")),
    )

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            compose_dir.mkdir(parents=True, exist_ok=True)
            (compose_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Warning: Armorer could not persist OpenClaw registry metadata." in last_payload.get("message", "")
    assert registry.get_app("openclaw") is not None
    assert registry.get_app("openclaw").install_path == str(tmp_path / "missing" / "openclaw")
    assert [args[:4] for args in run_calls] == [
        ["armorer", "install", "openclaw", "--yes"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_recovers_missing_registry_entry_after_install(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            compose_dir.mkdir(parents=True, exist_ok=True)
            (compose_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            app_meta = registry.get_app("openclaw")
            assert app_meta is not None
            assert app_meta.install_path == str(compose_dir)
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            app_meta = registry.get_app("openclaw")
            assert app_meta is not None
            assert app_meta.install_path == str(compose_dir)
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert registry.get_app("openclaw") is not None
    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "openai/existing-model"
    assert "OPENAI_API_KEY=existing-openai-key" in (compose_dir / ".openclaw" / ".env").read_text()
    assert [args[:4] for args in run_calls] == [
        ["armorer", "install", "openclaw", "--yes"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_reuses_existing_runtime_when_registry_metadata_is_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            raise AssertionError(f"Unexpected reinstall attempt: {args}")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Reused the existing OpenClaw install for this setup run." in last_payload.get("message", "")
    assert "Warning: Armorer could not persist OpenClaw registry metadata." not in last_payload.get("message", "")
    app_meta = AppRegistry().get_app("openclaw")
    assert app_meta is not None
    assert Path(app_meta.install_path) == compose_dir
    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("MODEL_PROVIDER") == "openai"
    assert cm.get_key("Armorer_MODEL") == "existing-model"
    assert cm.get_key("OPENAI_API_KEY") == "existing-openai-key"
    assert [args[:4] for args in run_calls] == [
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_succeeds_without_persisted_registry_metadata(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    compose_dir = tmp_path / "installed" / "openclaw"

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(
        handler.registry,
        "add_app",
        lambda app: (_ for _ in ()).throw(PermissionError("read-only registry")),
    )

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            compose_dir.mkdir(parents=True, exist_ok=True)
            (compose_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Warning: Armorer could not persist OpenClaw registry metadata." in last_payload.get("message", "")
    assert handler.registry.get_app("openclaw") is None
    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "openai/existing-model"
    assert "OPENAI_API_KEY=existing-openai-key" in (compose_dir / ".openclaw" / ".env").read_text()
    assert [args[:4] for args in run_calls] == [
        ["armorer", "install", "openclaw", "--yes"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_without_persisted_registry_metadata_still_supports_separate_run_status(
    tmp_path: Path, monkeypatch
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    compose_dir = tmp_path / "installed" / "openclaw"

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(
        handler.registry,
        "add_app",
        lambda app: (_ for _ in ()).throw(PermissionError("read-only registry")),
    )

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            compose_dir.mkdir(parents=True, exist_ok=True)
            (compose_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args == ["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Warning: Armorer could not persist OpenClaw registry metadata." in last_payload.get("message", "")
    assert handler.registry.get_app("openclaw") is None
    assert AppRegistry().get_app("openclaw") is None

    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.setattr("armorer.main.ConfigManager", lambda: SimpleNamespace())
    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)

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

    assert result.exit_code == 0
    assert "not found" not in result.stdout.lower()
    persisted_app = AppRegistry().get_app("openclaw")
    assert persisted_app is not None
    assert persisted_app.install_path == str(compose_dir)
    docker_calls = [args for args in run_calls if args[:2] == ["docker", "compose"]]
    assert docker_calls == [["docker", "compose", "run", "--rm", "--service-ports", "openclaw-cli", "status"]]


def test_openclaw_setup_flow_rejects_invalid_telegram_admin_ids(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: openclaw-model"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: abc,-hello"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "Telegram admin IDs must be numeric" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"


def test_openclaw_setup_flow_maps_gemini_provider_to_google_runtime_config(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: gemini"),
        ("oc3", "UI input: gemini-2.5-flash"),
        ("oc4", "UI input: gemini-test-key"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "google/gemini-2.5-flash"
    openclaw_env = (compose_dir / ".openclaw" / ".env").read_text()
    assert "GEMINI_API_KEY=gemini-test-key" in openclaw_env
    assert "GOOGLE_API_KEY=gemini-test-key" in openclaw_env


def test_openclaw_setup_flow_syncs_ollama_runtime_config(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: ollama"),
        ("oc3", "UI input: llama3.2"),
        ("oc4", "UI input: http://ollama-host:11434"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "ollama/llama3.2"
    assert cfg["models"]["providers"]["ollama"]["baseUrl"] == "http://ollama-host:11434"
    assert cfg["models"]["providers"]["ollama"]["apiKey"] == "ollama-local"
    assert cfg["models"]["providers"]["ollama"]["api"] == "ollama"
    openclaw_env = (compose_dir / ".openclaw" / ".env").read_text()
    assert "OLLAMA_API_KEY=ollama-local" in openclaw_env


def test_openclaw_setup_flow_prefixes_openrouter_model_refs(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openrouter"),
        ("oc3", "UI input: anthropic/claude-sonnet-4-5"),
        ("oc4", "UI input: sk-or-test-key"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "openrouter/anthropic/claude-sonnet-4-5"
    openclaw_env = (compose_dir / ".openclaw" / ".env").read_text()
    assert "OPENROUTER_API_KEY=sk-or-test-key" in openclaw_env


def test_openclaw_setup_flow_rejects_model_provider_prefix_mismatch(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: anthropic/claude-sonnet-4-5"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "model prefix targets anthropic" in last_payload.get("message", "")
    assert "selected provider is openai" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"
    assert last_payload.get("ui_action", {}).get("message") == "Enter model name"


def test_openclaw_setup_flow_rejects_blank_model_input(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: "),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "Enter a model name or reply `skip`" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"


def test_openclaw_setup_flow_rejects_blank_provider_key_input(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: "),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "Paste OPENAI_API_KEY, or reply `skip`" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"


def test_openclaw_setup_provider_key_step_answers_question_instead_of_advancing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: how do I get that?"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert last_payload.get("ui_action", {}).get("type") == "text"
    assert "openai dashboard" in last_payload.get("message", "").lower()
    assert "skip" in last_payload.get("message", "").lower()
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider_key"


def test_openclaw_setup_model_step_treats_openclaw_question_as_setup_help(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: what openclaw model should I use?"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert last_payload.get("ui_action", {}).get("type") == "text"
    assert "Enter the model name OpenClaw should use" in last_payload.get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "model"


def test_pending_openclaw_provider_key_step_routes_unrelated_question_to_llm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: what agents are currently running?"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert last_payload.get("message") == "llm_answer"
    assert "what agents are currently running?" in captured.get("user_input", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider_key"


def test_pending_openclaw_provider_step_routes_unrelated_question_to_llm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "what agents are currently running?"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert last_payload.get("message") == "llm_answer"
    assert "what agents are currently running?" in captured.get("user_input", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider"


def test_openclaw_setup_flow_rejects_blank_ollama_base_url(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: ollama"),
        ("oc3", "UI input: llama3.2"),
        ("oc4", "UI input: "),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "Enter the Ollama base URL or reply `skip`" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"


def test_openclaw_setup_flow_rejects_invalid_alert_chat_id(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc6", "UI input: 12345,-100999"),
        ("oc7", "UI input: not-a-chat-id"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "TELEGRAM_ALERT_CHAT_ID must be a numeric Telegram chat ID" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"


def test_openclaw_setup_flow_rejects_blank_alert_chat_without_explicit_skip(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc6", "UI input: 12345,-100999"),
        ("oc7", "UI input: "),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "Enter TELEGRAM_ALERT_CHAT_ID or reply `skip`" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"


def test_openclaw_setup_flow_rejects_blank_telegram_admin_ids(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc6", "UI input: "),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "Enter Telegram admin IDs as comma-separated numbers, or reply `skip`" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"


def test_openclaw_setup_flow_rejects_blank_armory_skills_input(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: "),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "Enter Armory skill names or reply `skip`" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"


def test_openclaw_setup_flow_rejects_blank_skill_paths_input(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: "),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "Enter skill directories or reply `skip`" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"


def test_openclaw_telegram_setup_request_uses_openclaw_flow_not_generic_telegram_flow(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "oc_tg.json").write_text(
        json.dumps(
            {
                "id": "oc_tg",
                "prompt": "setup telegram for openclaw",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc_tg.json"))
    payload = json.loads(json.loads((config_dir / "outbox" / "oc_tg.json").read_text())["response"])

    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("options") == ["ollama", "openai", "anthropic", "openrouter", "gemini", "skip"]
    assert "configure openclaw entirely from chat" in payload.get("message", "").lower()


def test_openclaw_setup_flow_rejects_unknown_armory_skill(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:3] == ["armorer", "run", "openclaw"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: openclaw-model"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: unknown-skill"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "Unknown Armory skill(s): unknown-skill" in last_payload.get("message", "")


def test_openclaw_setup_flow_model_skip_keeps_existing_model(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    handler.config_manager.set_key("Armorer_MODEL", "existing-model")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            return subprocess.CompletedProcess(args, 0, stdout="true" if args[8] == "channels.telegram.enabled" else "", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("Armorer_MODEL") == "existing-model"


def test_openclaw_setup_flow_confirmation_shows_preserved_runtime_values(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    handler.config_manager.set_key("TELEGRAM_BOT_TOKEN", "123456:abcDEF_token_sample_value")
    handler.config_manager.set_key("TELEGRAM_ALERT_CHAT_ID", "-100777")
    monkeypatch.setenv("ARMORER_SKILL_SCAN_PATHS", "existing/skills .openclaw/custom")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            injected_skills=["existing-skill"],
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    message = last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    assert "- provider: openai" in message
    assert "- model: existing-model" in message
    assert "- telegram: yes" in message
    assert "- telegram access: open" in message
    assert "- alert chat: -100777" in message
    assert "- armory skills: existing-skill" in message
    assert "- skill paths: existing/skills, .openclaw/custom" in message
    assert "missing prerequisites" not in message


def test_openclaw_setup_flow_confirmation_warns_about_missing_provider_key(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    message = last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    assert "- provider: openai" in message
    assert "- model: gpt-4.1-mini" in message
    assert "- missing prerequisites: OPENAI_API_KEY" in message
    assert "prompt for any missing required values before install" in message


def test_openclaw_setup_flow_verifies_open_group_policy_when_admins_skipped(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS", "false")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    config_values: dict[str, object] = {}
    get_calls: list[str] = []

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            key = args[8]
            if op == "set":
                value = args[9]
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                get_calls.append(key)
                value = config_values.get(key, "")
                if isinstance(value, list):
                    return subprocess.CompletedProcess(args, 0, stdout=json.dumps(value), stderr="")
                return subprocess.CompletedProcess(args, 0, stdout=str(value), stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI input: skip"),
        ("oc10", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert config_values["channels.telegram.groupPolicy"] == "open"
    assert "channels.telegram.groupPolicy" in get_calls
    assert handler.config_manager.get_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS") == "false"


def test_openclaw_setup_flow_provider_key_skip_keeps_existing_key(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("OPENAI_API_KEY", "existing-openai-key")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("OPENAI_API_KEY") == "existing-openai-key"


def test_openclaw_setup_flow_reprompts_for_missing_provider_at_confirm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OPENAI_API_KEY",
        "ANTHROPIC_API_KEY",
        "OPENROUTER_API_KEY",
        "GEMINI_API_KEY",
        "GOOGLE_API_KEY",
    ):
        monkeypatch.delenv(key, raising=False)

    def _should_not_run(*args, **kwargs):
        raise AssertionError("Install should not start without an effective provider.")

    monkeypatch.setattr(subprocess, "run", _should_not_run)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "still needs a model provider before install" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "select"
    assert "openai" in last_payload.get("ui_action", {}).get("options", [])
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider"


def test_openclaw_setup_flow_reprompts_for_missing_remote_provider_key_at_confirm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    def _should_not_run(*args, **kwargs):
        raise AssertionError("Install should not start without the required provider key.")

    monkeypatch.setattr(subprocess, "run", _should_not_run)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "still needs OPENAI_API_KEY before install" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"
    assert last_payload.get("ui_action", {}).get("message") == "Enter OPENAI_API_KEY"
    assert last_payload.get("ui_action", {}).get("sensitive") is True
    assert "default" not in (last_payload.get("ui_action", {}) or {})
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider_key"


def test_openclaw_setup_flow_reprompts_for_missing_model_at_confirm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "Armorer_MODEL",
        "OPENAI_API_KEY",
    ):
        monkeypatch.delenv(key, raising=False)

    def _should_not_run(*args, **kwargs):
        raise AssertionError("Install should not start without an effective model.")

    monkeypatch.setattr(subprocess, "run", _should_not_run)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "still needs a model name before install" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"
    assert last_payload.get("ui_action", {}).get("message") == "Enter model name"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "model"


def test_openclaw_setup_flow_reprompts_for_model_provider_mismatch_at_confirm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("MODEL_PROVIDER", "openai")
    handler.config_manager.set_key("Armorer_MODEL", "anthropic/claude-sonnet-4-5")
    handler.config_manager.set_key("OPENAI_API_KEY", "sk-existing-openai")

    def _should_not_run(*args, **kwargs):
        raise AssertionError("Install should not start while model/provider mismatch is unresolved.")

    monkeypatch.setattr(subprocess, "run", _should_not_run)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "still needs a openai model before install" in last_payload.get("message", "")
    assert "current model prefix targets anthropic" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "text"
    assert last_payload.get("ui_action", {}).get("message") == "Enter model name"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("type") == "openclaw_setup"
    assert pending.get("step") == "model"
    assert pending.get("state", {}).get("resume_to_confirm") is True


def test_openclaw_setup_flow_collects_missing_provider_key_at_confirm_and_resumes_confirmation(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    install_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            install_attempts["count"] += 1
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
        ("oc9", "UI input: sk-test-openai"),
        ("oc10", "UI confirmation: yes"),
    ]

    payloads = {}
    pending_steps = {}
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])
        pending_steps[task_id] = dict(handler._pending_decisions.get(cid, {}))

    assert "still needs OPENAI_API_KEY before install" in payloads["oc8"].get("message", "")
    assert payloads["oc8"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc8"].get("ui_action", {}).get("sensitive") is True
    assert pending_steps["oc8"].get("step") == "provider_key"
    assert pending_steps["oc8"].get("state", {}).get("resume_to_confirm") is True
    assert payloads["oc9"].get("ui_action", {}).get("type") == "confirm"
    assert "- provider: openai" in payloads["oc9"].get("message", "")
    assert "- missing prerequisites" not in payloads["oc9"].get("message", "")
    assert pending_steps["oc9"].get("step") == "confirm"
    assert pending_steps["oc9"].get("state", {}).get("resume_to_confirm") is None
    assert "OpenClaw is installed and verified" in payloads["oc10"].get("message", "")
    assert "OpenClaw status: healthy" in payloads["oc10"].get("message", "")
    assert install_attempts["count"] == 1


def test_reprompt_openclaw_text_step_omits_default_for_sensitive_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    _, payload = handler._reprompt_openclaw_text_step(
        "conv-sensitive",
        "provider_key",
        {"alias": "openclaw"},
        message="Paste OPENAI_API_KEY.",
        prompt_message="Enter OPENAI_API_KEY",
        default="sk-existing-secret",
    )

    assert payload.get("ui_action", {}).get("type") == "text"
    assert payload.get("ui_action", {}).get("message") == "Enter OPENAI_API_KEY"
    assert payload.get("ui_action", {}).get("sensitive") is True
    assert "default" not in (payload.get("ui_action", {}) or {})
    assert handler._pending_decisions.get("conv-sensitive", {}).get("step") == "provider_key"


def test_reprompt_openclaw_text_step_omits_default_for_sensitive_step_with_generic_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    _, payload = handler._reprompt_openclaw_text_step(
        "conv-sensitive-step",
        "telegram_token",
        {"alias": "openclaw"},
        message="Paste token.",
        prompt_message="Enter value",
        default="123456:abcDEF_token_sample_value",
    )

    assert payload.get("ui_action", {}).get("type") == "text"
    assert payload.get("ui_action", {}).get("message") == "Enter value"
    assert payload.get("ui_action", {}).get("sensitive") is True
    assert "default" not in (payload.get("ui_action", {}) or {})
    assert handler._pending_decisions.get("conv-sensitive-step", {}).get("step") == "telegram_token"


def test_reprompt_openclaw_text_step_keeps_default_for_non_sensitive_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    _, payload = handler._reprompt_openclaw_text_step(
        "conv-model",
        "model",
        {"alias": "openclaw"},
        message="Enter model name.",
        prompt_message="Enter model name",
        default="llama3.2",
    )

    assert payload.get("ui_action", {}).get("type") == "text"
    assert payload.get("ui_action", {}).get("message") == "Enter model name"
    assert payload.get("ui_action", {}).get("sensitive") is not True
    assert payload.get("ui_action", {}).get("default") == "llama3.2"
    assert handler._pending_decisions.get("conv-model", {}).get("step") == "model"


def test_openclaw_setup_flow_reuses_existing_openclaw_runtime_when_armorer_config_is_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OPENAI_API_KEY",
    ):
        monkeypatch.delenv(key, raising=False)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Provider: unchanged (openai)" in last_payload.get("message", "")
    cfg = json.loads((cfg_dir / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "openai/existing-model"
    assert "OPENAI_API_KEY=existing-openai-key" in (cfg_dir / ".env").read_text()
    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("MODEL_PROVIDER") == "openai"
    assert cm.get_key("Armorer_MODEL") == "existing-model"
    assert cm.get_key("OPENAI_API_KEY") == "existing-openai-key"
    assert cm.get_key("ARMORER_SKILL_SCAN_PATHS") == "skills .openclaw/skills"


def test_openclaw_setup_flow_displays_gemini_for_reused_google_runtime(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "GEMINI_API_KEY",
        "GOOGLE_API_KEY",
    ):
        monkeypatch.delenv(key, raising=False)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "google/existing-gemini-model"}}},
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("GEMINI_API_KEY=existing-gemini-key\nGOOGLE_API_KEY=existing-gemini-key\n")
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    cm = ConfigManager()
    cm.use_keyring = False
    assert "- provider: gemini" in payloads["oc6"].get("message", "")
    assert "Provider: unchanged (gemini)" in payloads["oc7"].get("message", "")
    assert cm.get_key("MODEL_PROVIDER") == "gemini"
    assert cm.get_key("Armorer_MODEL") == "existing-gemini-model"
    assert cm.get_key("GEMINI_API_KEY") == "existing-gemini-key"


def test_resolve_openclaw_runtime_settings_prefers_installed_runtime_over_stale_armorer_config(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("MODEL_PROVIDER", "openai")
    handler.config_manager.set_key("Armorer_MODEL", "stale-model")
    handler.config_manager.set_key("OPENAI_API_KEY", "stale-openai-key")
    handler.config_manager.set_key("TELEGRAM_ALERT_CHAT_ID", "-100stale")
    handler.config_manager.set_key("ARMORER_SKILL_SCAN_PATHS", "stale/skills")

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}},
                "skills": {"load": {"extraDirs": ["runtime/skills", ".openclaw/runtime"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (compose_dir / ".env").write_text("TELEGRAM_ALERT_CHAT_ID=-100777\n")
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    runtime = handler._resolve_openclaw_runtime_settings({"alias": "openclaw", "provider": "skip"})

    assert runtime["provider"] == "openai"
    assert runtime["model"] == "runtime-model"
    assert runtime["model_ref"] == "openai/runtime-model"
    assert runtime["provider_key_name"] == "OPENAI_API_KEY"
    assert runtime["provider_key_value"] == "runtime-openai-key"
    assert runtime["alert_chat_id"] == "-100777"
    assert runtime["skill_paths"] == ["runtime/skills", ".openclaw/runtime"]


def test_resolve_openclaw_runtime_settings_falls_back_when_registry_path_is_stale(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("MODEL_PROVIDER", "openai")
    handler.config_manager.set_key("Armorer_MODEL", "stale-model")
    handler.config_manager.set_key("OPENAI_API_KEY", "stale-openai-key")
    handler.config_manager.set_key("ARMORER_SKILL_SCAN_PATHS", "stale/skills")

    registry = AppRegistry()
    live_compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = live_compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}},
                "skills": {"load": {"extraDirs": ["runtime/skills", ".openclaw/runtime"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (live_compose_dir / ".env").write_text("TELEGRAM_ALERT_CHAT_ID=-100777\n")
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(tmp_path / "missing" / "openclaw"),
            agent_id="openclaw",
        )
    )

    runtime = handler._resolve_openclaw_runtime_settings({"alias": "openclaw", "provider": "skip"})

    assert runtime["provider"] == "openai"
    assert runtime["model"] == "runtime-model"
    assert runtime["provider_key_name"] == "OPENAI_API_KEY"
    assert runtime["provider_key_value"] == "runtime-openai-key"
    assert runtime["alert_chat_id"] == "-100777"
    assert runtime["skill_paths"] == ["runtime/skills", ".openclaw/runtime"]


def test_resolve_openclaw_runtime_settings_does_not_invent_none_skill_path(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in ("ARMORER_SKILL_SCAN_PATHS", "TELEGRAM_ALERT_CHAT_ID"):
        monkeypatch.delenv(key, raising=False)

    runtime = handler._resolve_openclaw_runtime_settings(
        {
            "alias": "openclaw",
            "provider": "openai",
            "model": "runtime-model",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "runtime-openai-key",
        }
    )

    assert runtime["skill_paths"] == []


def test_resolve_openclaw_runtime_settings_allows_clearing_optional_runtime_state(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("MODEL_PROVIDER", "openai")
    handler.config_manager.set_key("Armorer_MODEL", "runtime-model")
    handler.config_manager.set_key("OPENAI_API_KEY", "runtime-openai-key")
    handler.config_manager.set_key("TELEGRAM_ALERT_CHAT_ID", "-100stale")
    handler.config_manager.set_key("ARMORER_SKILL_SCAN_PATHS", "stale/skills")

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}},
                "skills": {"load": {"extraDirs": ["runtime/skills", ".openclaw/runtime"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (compose_dir / ".env").write_text("TELEGRAM_ALERT_CHAT_ID=-100777\n")
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    runtime = handler._resolve_openclaw_runtime_settings(
        {
            "alias": "openclaw",
            "provider": "skip",
            "clear_alert_chat_id": True,
            "clear_skill_paths": True,
        }
    )

    assert runtime["alert_chat_id"] == ""
    assert runtime["skill_paths"] == []
    assert "TELEGRAM_ALERT_CHAT_ID" not in runtime["env_updates"]


def test_resolve_openclaw_runtime_settings_recomputes_provider_key_when_provider_changes(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("OPENAI_API_KEY", "fresh-openai-key")

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "ollama/existing-model"}}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OLLAMA_API_KEY=ollama-local\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    runtime = handler._resolve_openclaw_runtime_settings(
        {
            "alias": "openclaw",
            "provider": "openai",
            "model": "gpt-4.1-mini",
        }
    )

    assert runtime["provider"] == "openai"
    assert runtime["provider_key_name"] == "OPENAI_API_KEY"
    assert runtime["provider_key_value"] == "fresh-openai-key"
    assert runtime["env_updates"] == {"OPENAI_API_KEY": "fresh-openai-key"}


def test_sync_openclaw_armory_skills_can_clear_existing_managed_skills(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "mcpServers": {
                    "existing-skill": {"command": "npx", "args": ["-y", "existing-skill"]},
                    "custom-remote": {"command": "npx", "args": ["-y", "custom-remote"]},
                }
            },
            indent=2,
        )
        + "\n"
    )
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            injected_skills=["existing-skill"],
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    applied = handler._sync_openclaw_armory_skills(compose_dir, "openclaw", [], clear_existing=True)

    cfg = json.loads((cfg_dir / "openclaw.json").read_text())
    assert applied == []
    assert "existing-skill" not in cfg.get("mcpServers", {})
    assert "custom-remote" in cfg.get("mcpServers", {})
    app_meta = registry.get_app("openclaw")
    assert app_meta is not None
    assert app_meta.injected_skills == []


def test_sync_openclaw_armory_skills_tolerates_registry_save_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text("{}\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            injected_skills=[],
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return [SimpleNamespace(name="filesystem")]

        def get_skill(self, name: str):
            if name != "filesystem":
                return None
            return SimpleNamespace(
                name="filesystem",
                config={"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]},
            )

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(handler.registry, "add_app", lambda app: (_ for _ in ()).throw(PermissionError("read-only registry")))

    applied = handler._sync_openclaw_armory_skills(compose_dir, "openclaw", ["filesystem"])

    cfg = json.loads((cfg_dir / "openclaw.json").read_text())
    assert applied == ["filesystem"]
    assert "filesystem" in cfg.get("mcpServers", {})
    assert handler._existing_openclaw_injected_skills("openclaw") == ["filesystem"]


def test_verify_openclaw_setup_rejects_lingering_managed_skill_after_clear(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}},
                "mcpServers": {
                    "existing-skill": {"command": "npx", "args": ["-y", "existing-skill"]},
                },
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            injected_skills=[],
        )
    )

    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda *args, **kwargs: subprocess.CompletedProcess(args[0], 0, stdout="OpenClaw status: healthy", stderr=""),
    )
    monkeypatch.setattr(
        handler,
        "_list_armory_skill_names",
        lambda: ["existing-skill"],
    )

    with pytest.raises(RuntimeError, match=r"Injected OpenClaw skill clear verification failed for: existing-skill\."):
        handler._verify_openclaw_setup(
            "openclaw",
            {
                "alias": "openclaw",
                "provider": "openai",
                "model": "runtime-model",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "runtime-openai-key",
                "clear_armory_skills": True,
                "clear_skill_paths": True,
                "cleared_armory_skills": "existing-skill",
            },
            "task-verify",
        )


def test_existing_openclaw_telegram_state_prefers_installed_runtime_over_stale_armorer_config(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("TELEGRAM_BOT_TOKEN", "123456:stale_token_value")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_USER_IDS", "111")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS", "111")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS", "false")
    handler.config_manager.set_key("TELEGRAM_ALERT_CHAT_ID", "-100stale")

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "channels": {
                    "telegram": {
                        "botToken": "123456:runtime_token_value",
                        "enabled": True,
                        "groupPolicy": "allowlist",
                        "allowFrom": ["12345", "-100999"],
                        "groupAllowFrom": ["12345", "-100999"],
                    }
                }
            },
            indent=2,
        )
        + "\n"
    )
    (compose_dir / ".env").write_text("TELEGRAM_ALERT_CHAT_ID=-100777\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    state = handler._existing_openclaw_telegram_state("openclaw")

    assert state == {
        "telegram_token": "123456:runtime_token_value",
        "telegram_admin_ids": "12345,-100999",
        "alert_chat_id": "-100777",
    }


def test_verify_openclaw_setup_rejects_missing_runtime_env_updates(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in ("MODEL_PROVIDER", "Armorer_MODEL", "OPENAI_API_KEY", "TELEGRAM_ALERT_CHAT_ID", "ARMORER_SKILL_SCAN_PATHS"):
        monkeypatch.delenv(key, raising=False)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda *args, **kwargs: subprocess.CompletedProcess(args[0], 0, stdout="OpenClaw status: healthy", stderr=""),
    )

    with pytest.raises(RuntimeError, match=r"OpenClaw runtime env verification failed for: OPENAI_API_KEY, TELEGRAM_ALERT_CHAT_ID\."):
        handler._verify_openclaw_setup(
            "openclaw",
            {
                "alias": "openclaw",
                "provider": "openai",
                "model": "runtime-model",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "runtime-openai-key",
                "alert_chat_id": "-100777",
            },
            "task-verify",
        )


def test_verify_openclaw_setup_accepts_runtime_env_updates_from_install_env_files(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in ("MODEL_PROVIDER", "Armorer_MODEL", "OPENAI_API_KEY", "TELEGRAM_ALERT_CHAT_ID", "ARMORER_SKILL_SCAN_PATHS"):
        monkeypatch.delenv(key, raising=False)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\nTELEGRAM_ALERT_CHAT_ID=-100777\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda *args, **kwargs: subprocess.CompletedProcess(args[0], 0, stdout="OpenClaw status: healthy", stderr=""),
    )

    status_text = handler._verify_openclaw_setup(
        "openclaw",
        {
            "alias": "openclaw",
            "provider": "openai",
            "model": "runtime-model",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "runtime-openai-key",
            "alert_chat_id": "-100777",
        },
        "task-verify",
    )

    assert "OpenClaw status: healthy" in status_text


def test_verify_openclaw_setup_truncates_to_latest_status_lines(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in ("MODEL_PROVIDER", "Armorer_MODEL", "OPENAI_API_KEY"):
        monkeypatch.delenv(key, raising=False)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    noisy = ("Security Event: stale monitor alert\n" * 120) + "OpenClaw status: healthy\nFinal check: ok\n"
    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda *args, **kwargs: subprocess.CompletedProcess(args[0], 0, stdout=noisy, stderr=""),
    )

    status_text = handler._verify_openclaw_setup(
        "openclaw",
        {
            "alias": "openclaw",
            "provider": "openai",
            "model": "runtime-model",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "runtime-openai-key",
        },
        "task-verify",
    )

    assert status_text.startswith("[output truncated; showing latest lines]\n")
    assert "OpenClaw status: healthy" in status_text
    assert "Final check: ok" in status_text
    assert len(status_text) <= 1200


def test_verify_openclaw_setup_sanitizes_secret_like_status_output(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in ("MODEL_PROVIDER", "Armorer_MODEL", "OPENAI_API_KEY"):
        monkeypatch.delenv(key, raising=False)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda *args, **kwargs: subprocess.CompletedProcess(
            args[0],
            0,
            stdout=f"OpenClaw status: healthy\nprovider key={leaked_key}\ntelegram token={leaked_token}\n",
            stderr="",
        ),
    )

    status_text = handler._verify_openclaw_setup(
        "openclaw",
        {
            "alias": "openclaw",
            "provider": "openai",
            "model": "runtime-model",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "runtime-openai-key",
        },
        "task-verify",
    )

    assert "OpenClaw status: healthy" in status_text
    assert "[REDACTED_OPENAI_KEY]" in status_text
    assert "[REDACTED_TELEGRAM_TOKEN]" in status_text
    assert leaked_key not in status_text
    assert leaked_token not in status_text


def test_verify_openclaw_setup_rejects_model_provider_mismatch(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "anthropic/claude-sonnet-4-5"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("status command should not run")),
    )

    with pytest.raises(
        RuntimeError,
        match=r"OpenClaw model/provider verification failed: the configured model targets anthropic, but the selected provider is openai\.",
        ):
            handler._verify_openclaw_setup(
                "openclaw",
                {
                    "alias": "openclaw",
                "provider": "openai",
                "model": "anthropic/claude-sonnet-4-5",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "runtime-openai-key",
                },
                "task-verify",
            )


def test_verify_openclaw_setup_rejects_missing_skill_directory_paths(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}},
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text(
        "OPENAI_API_KEY=runtime-openai-key\nARMORER_SKILL_SCAN_PATHS=skills .openclaw/skills\n"
    )
    _materialize_skill_dirs(compose_dir, "skills")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    monkeypatch.setattr(
        handler,
        "_run_openclaw_setup_command",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("status command should not run")),
    )

    with pytest.raises(
        RuntimeError,
        match=r"OpenClaw skill directory verification failed; missing directories: \.openclaw/skills\.",
    ):
        handler._verify_openclaw_setup(
            "openclaw",
            {
                "alias": "openclaw",
                "provider": "openai",
                "model": "runtime-model",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "runtime-openai-key",
                "skills_paths": "skills,.openclaw/skills",
            },
            "task-verify",
        )


def test_verify_openclaw_setup_runs_registered_openclaw_health_check(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            run_commands=[
                {
                    "label": "Health check",
                    "service": "openclaw-cli",
                    "command": "doctor",
                    "category": "run",
                    "description": "Run OpenClaw health checks.",
                }
            ],
        )
    )

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

    def _fake_run(args, **kwargs):
        calls.append(list(args))
        if args[-1] == "status":
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[-1] == "doctor":
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw doctor: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(handler, "_run_openclaw_setup_command", _fake_run)

    status_text = handler._verify_openclaw_setup(
        "openclaw",
        {
            "alias": "openclaw",
            "provider": "openai",
            "model": "runtime-model",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "runtime-openai-key",
        },
        "task-verify",
    )

    assert calls == [["run", "openclaw", "status"], ["run", "openclaw", "doctor"]]
    assert "OpenClaw status: healthy" in status_text
    assert "OpenClaw doctor: healthy" in status_text


def test_openclaw_registry_health_check_detection_falls_back_to_playbook_when_registry_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    playbook = SimpleNamespace(
        compose_subdir=".",
        run_commands=[SimpleNamespace(command="doctor")],
    )
    monkeypatch.setattr(core_mod, "resolve_playbook", lambda name: playbook if name == "openclaw" else None, raising=True)

    assert handler._openclaw_registry_has_health_check("openclaw") is True


def test_verify_openclaw_setup_runs_playbook_health_check_when_registry_metadata_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    playbook = SimpleNamespace(
        compose_subdir=".",
        run_commands=[SimpleNamespace(command="doctor")],
    )
    monkeypatch.setattr(core_mod, "resolve_playbook", lambda name: playbook if name == "openclaw" else None, raising=True)

    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")

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

    def _fake_run(args, **kwargs):
        calls.append(list(args))
        if args[-1] == "status":
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[-1] == "doctor":
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw doctor: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(handler, "_run_openclaw_setup_command", _fake_run)

    status_text = handler._verify_openclaw_setup(
        "openclaw",
        {
            "alias": "openclaw",
            "provider": "openai",
            "model": "runtime-model",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "runtime-openai-key",
        },
        "task-verify",
    )

    assert calls == [["run", "openclaw", "status"], ["run", "openclaw", "doctor"]]
    assert "OpenClaw status: healthy" in status_text
    assert "OpenClaw doctor: healthy" in status_text


def test_verify_openclaw_setup_runs_playbook_health_check_for_non_default_alias_without_registry_metadata(
    tmp_path: Path, monkeypatch
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    playbook = SimpleNamespace(
        compose_subdir=".",
        run_commands=[SimpleNamespace(command="doctor")],
    )
    monkeypatch.setattr(core_mod, "resolve_playbook", lambda name: playbook if name == "openclaw" else None, raising=True)

    compose_dir = tmp_path / "installed" / "prindleclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")

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

    def _fake_run(args, **kwargs):
        calls.append(list(args))
        if args[-1] == "status":
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[-1] == "doctor":
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw doctor: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(handler, "_run_openclaw_setup_command", _fake_run)

    status_text = handler._verify_openclaw_setup(
        "prindleclaw",
        {
            "alias": "prindleclaw",
            "provider": "openai",
            "model": "runtime-model",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "runtime-openai-key",
        },
        "task-verify",
    )

    assert calls == [["run", "prindleclaw", "status"], ["run", "prindleclaw", "doctor"]]
    assert "OpenClaw status: healthy" in status_text
    assert "OpenClaw doctor: healthy" in status_text


def test_verify_openclaw_setup_surfaces_registered_openclaw_health_check_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            run_commands=[
                {
                    "label": "Health check",
                    "service": "openclaw-cli",
                    "command": "doctor",
                    "category": "run",
                    "description": "Run OpenClaw health checks.",
                }
            ],
        )
    )

    def _fake_run(args, **kwargs):
        if args[-1] == "status":
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[-1] == "doctor":
            raise RuntimeError("Running OpenClaw health check failed: provider authentication failed")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(handler, "_run_openclaw_setup_command", _fake_run)

    with pytest.raises(RuntimeError, match=r"Running OpenClaw health check failed: provider authentication failed"):
        handler._verify_openclaw_setup(
            "openclaw",
            {
                "alias": "openclaw",
                "provider": "openai",
                "model": "runtime-model",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "runtime-openai-key",
            },
            "task-verify",
        )


def test_verify_openclaw_setup_skips_unavailable_registered_openclaw_health_check(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            run_commands=[
                {
                    "label": "Health check",
                    "service": "openclaw-cli",
                    "command": "doctor",
                    "category": "run",
                    "description": "Run OpenClaw health checks.",
                }
            ],
        )
    )

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

    def _fake_run(args, **kwargs):
        calls.append(list(args))
        if args[-1] == "status":
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[-1] == "doctor":
            raise RuntimeError("Running OpenClaw health check failed: unexpected command `doctor`")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(handler, "_run_openclaw_setup_command", _fake_run)

    status_text = handler._verify_openclaw_setup(
        "openclaw",
        {
            "alias": "openclaw",
            "provider": "openai",
            "model": "runtime-model",
            "provider_key_name": "OPENAI_API_KEY",
            "provider_key_value": "runtime-openai-key",
        },
        "task-verify",
    )

    assert calls == [["run", "openclaw", "status"], ["run", "openclaw", "doctor"]]
    assert "OpenClaw status: healthy" in status_text
    assert "doctor" not in status_text.lower()


def test_openclaw_health_check_unavailable_matches_command_errors_only(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)

    assert handler._openclaw_health_check_unavailable(RuntimeError("unexpected command `doctor`")) is True
    assert handler._openclaw_health_check_unavailable(RuntimeError("unknown run command: doctor")) is True
    assert handler._openclaw_health_check_unavailable(RuntimeError("doctor command not found")) is True
    assert handler._openclaw_health_check_unavailable(RuntimeError("/bin/sh: doctor: not found")) is True
    assert handler._openclaw_health_check_unavailable(RuntimeError("`doctor` not found in this runtime")) is True
    assert handler._openclaw_health_check_unavailable(RuntimeError("doctor is not a recognized command")) is True
    assert handler._openclaw_health_check_unavailable(
        RuntimeError("'doctor' is not recognized as an internal or external command")
    ) is True
    assert handler._openclaw_health_check_unavailable(RuntimeError("invalid choice: 'doctor'")) is True
    assert handler._openclaw_health_check_unavailable(RuntimeError("unsupported command `doctor`")) is True
    assert handler._openclaw_health_check_unavailable(
        RuntimeError("Running OpenClaw health check failed: invalid choice: 'openai'")
    ) is False
    assert handler._openclaw_health_check_unavailable(
        RuntimeError("Running OpenClaw health check failed: provider health: not found")
    ) is False
    assert handler._openclaw_health_check_unavailable(RuntimeError("provider model not found")) is False
    assert handler._openclaw_health_check_unavailable(
        RuntimeError("'openai' is not recognized as an internal or external command")
    ) is False
    assert handler._openclaw_health_check_unavailable(RuntimeError("unsupported provider configuration")) is False


def test_verify_openclaw_setup_does_not_swallow_registered_openclaw_health_check_model_not_found_failure(
    tmp_path: Path, monkeypatch
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps({"agents": {"defaults": {"model": {"primary": "openai/runtime-model"}}}}, indent=2) + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=runtime-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            run_commands=[
                {
                    "label": "Health check",
                    "service": "openclaw-cli",
                    "command": "doctor",
                    "category": "run",
                    "description": "Run OpenClaw health checks.",
                }
            ],
        )
    )

    def _fake_run(args, **kwargs):
        if args[-1] == "status":
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[-1] == "doctor":
            raise RuntimeError("Running OpenClaw health check failed: provider model not found")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(handler, "_run_openclaw_setup_command", _fake_run)

    with pytest.raises(RuntimeError, match=r"provider model not found"):
        handler._verify_openclaw_setup(
            "openclaw",
            {
                "alias": "openclaw",
                "provider": "openai",
                "model": "runtime-model",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "runtime-openai-key",
            },
            "task-verify",
        )


def test_verify_openclaw_telegram_channel_uses_telegram_api_for_token_and_chat_ids(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured = {"token": "", "chat_ids": []}

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            captured["token"] = str(bot_token or "")

        def get_me(self):
            return {"id": 1, "username": "verified_bot"}

        def get_chat(self, chat_id: str):
            captured["chat_ids"].append(str(chat_id))
            return {"id": str(chat_id), "type": "supergroup"}

    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)

    verification = handler._verify_openclaw_telegram_channel(
        "openclaw",
        {
            "telegram_token": "123456:abcDEF_token_sample_value",
            "telegram_admin_ids": "12345,-100999",
            "alert_chat_id": "-100777",
        },
        {"alert_chat_id": "-100777"},
        "task-verify",
    )

    assert captured["token"] == "123456:abcDEF_token_sample_value"
    assert captured["chat_ids"] == ["12345", "-100999", "-100777"]
    assert verification == {
        "bot_username": "verified_bot",
        "verified_chat_ids": ["12345", "-100999", "-100777"],
    }


def test_verify_openclaw_telegram_channel_raises_for_invalid_chat_verification(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "verified_bot"}

        def get_chat(self, chat_id: str):
            raise RuntimeError("chat not found")

    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)

    with pytest.raises(RuntimeError, match=r"Telegram chat verification failed for -100777: chat not found"):
        handler._verify_openclaw_telegram_channel(
            "openclaw",
            {
                "telegram_token": "123456:abcDEF_token_sample_value",
                "alert_chat_id": "-100777",
            },
            {"alert_chat_id": "-100777"},
            "task-verify",
        )


def test_verify_openclaw_telegram_channel_uses_exception_class_when_get_me_error_is_blank(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            raise RuntimeError()

        def get_chat(self, chat_id: str):
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    with pytest.raises(RuntimeError, match=r"Telegram bot token verification failed: RuntimeError"):
        handler._verify_openclaw_telegram_channel(
            "openclaw",
            {
                "telegram_token": "123456:abcDEF_token_sample_value",
                "alert_chat_id": "12345",
            },
            {"alert_chat_id": "12345"},
            "task-verify",
        )


def test_verify_openclaw_telegram_channel_redacts_sk_proj_key_in_get_me_error(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            raise RuntimeError(f"invalid api key {leaked_key}")

        def get_chat(self, chat_id: str):
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    with pytest.raises(RuntimeError) as excinfo:
        handler._verify_openclaw_telegram_channel(
            "openclaw",
            {
                "telegram_token": "123456:abcDEF_token_sample_value",
                "alert_chat_id": "12345",
            },
            {"alert_chat_id": "12345"},
            "task-verify",
        )
    message = str(excinfo.value)
    assert "Telegram bot token verification failed:" in message
    assert "[REDACTED_OPENAI_KEY]" in message
    assert leaked_key not in message


def test_verify_openclaw_telegram_channel_redacts_sk_proj_key_in_get_chat_error(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "verified_bot"}

        def get_chat(self, chat_id: str):
            raise RuntimeError(f"chat verification failed with key {leaked_key}")

    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    with pytest.raises(RuntimeError) as excinfo:
        handler._verify_openclaw_telegram_channel(
            "openclaw",
            {
                "telegram_token": "123456:abcDEF_token_sample_value",
                "alert_chat_id": "-100777",
            },
            {"alert_chat_id": "-100777"},
            "task-verify",
        )
    message = str(excinfo.value)
    assert "Telegram chat verification failed for -100777:" in message
    assert "[REDACTED_OPENAI_KEY]" in message
    assert leaked_key not in message


def test_verify_openclaw_telegram_channel_verifies_positive_alert_chat_id(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: list[str] = []

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "verified_bot"}

        def get_chat(self, chat_id: str):
            captured.append(str(chat_id))
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)

    verification = handler._verify_openclaw_telegram_channel(
        "openclaw",
        {
            "telegram_token": "123456:abcDEF_token_sample_value",
            "alert_chat_id": "12345",
        },
        {"alert_chat_id": "12345"},
        "task-verify",
    )

    assert captured == ["12345"]
    assert verification["verified_chat_ids"] == ["12345"]


def test_verify_openclaw_telegram_channel_retries_transient_get_me_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    attempts = {"get_me": 0, "get_chat": 0}

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            attempts["get_me"] += 1
            if attempts["get_me"] == 1:
                raise RuntimeError("temporary telegram API timeout")
            return {"id": 1, "username": "verified_bot"}

        def get_chat(self, chat_id: str):
            attempts["get_chat"] += 1
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)
    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    verification = handler._verify_openclaw_telegram_channel(
        "openclaw",
        {
            "telegram_token": "123456:abcDEF_token_sample_value",
            "alert_chat_id": "12345",
        },
        {"alert_chat_id": "12345"},
        "task-verify",
    )

    assert attempts["get_me"] == 2
    assert attempts["get_chat"] == 1
    assert verification == {
        "bot_username": "verified_bot",
        "verified_chat_ids": ["12345"],
    }
    assert any("Verifying Telegram bot token with Telegram API (attempt 1/3)" in line for line in status_updates)
    assert any("failed, retrying: temporary telegram API timeout" in line for line in status_updates)


def test_verify_openclaw_telegram_channel_retries_transient_get_chat_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    attempts = {"get_chat": 0}

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "verified_bot"}

        def get_chat(self, chat_id: str):
            attempts["get_chat"] += 1
            if attempts["get_chat"] == 1:
                raise RuntimeError("temporary getChat failure")
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)
    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    verification = handler._verify_openclaw_telegram_channel(
        "openclaw",
        {
            "telegram_token": "123456:abcDEF_token_sample_value",
            "alert_chat_id": "12345",
        },
        {"alert_chat_id": "12345"},
        "task-verify",
    )

    assert attempts["get_chat"] == 2
    assert verification == {
        "bot_username": "verified_bot",
        "verified_chat_ids": ["12345"],
    }
    assert any("Verifying Telegram chat 12345 (attempt 1/3)" in line for line in status_updates)
    assert any("failed, retrying: temporary getChat failure" in line for line in status_updates)


def test_verify_openclaw_telegram_channel_preserves_superseded_task_error(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    superseded = (
        "Skipped this setup because a newer request from the same chat/session was submitted. "
        "Continuing with the latest task `new`."
    )

    monkeypatch.setattr(
        handler,
        "_run_openclaw_telegram_verification_step",
        lambda *args, **kwargs: (_ for _ in ()).throw(SupersededTaskError(superseded)),
        raising=True,
    )

    with pytest.raises(SupersededTaskError, match=r"latest task `new`"):
        handler._verify_openclaw_telegram_channel(
            "openclaw",
            {
                "telegram_token": "123456:abcDEF_token_sample_value",
                "alert_chat_id": "12345",
            },
            {"alert_chat_id": "12345"},
            "task-verify",
        )


def test_run_openclaw_telegram_verification_step_aborts_on_superseded_task(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    attempts = {"count": 0}
    superseded = (
        "Skipped this setup because a newer request from the same chat/session was submitted. "
        "Continuing with the latest task `new`."
    )

    def _superseded_call() -> object:
        attempts["count"] += 1
        raise SupersededTaskError(superseded)

    with pytest.raises(SupersededTaskError, match=r"latest task `new`"):
        handler._run_openclaw_telegram_verification_step(
            "old",
            "Verifying Telegram bot token with Telegram API",
            _superseded_call,
            retries=3,
        )

    assert attempts["count"] == 1
    assert not any("failed, retrying" in line for line in status_updates)


def test_run_openclaw_telegram_verification_step_aborts_when_superseded_during_retry_note(
    tmp_path: Path, monkeypatch
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    superseded = (
        "Skipped this setup because a newer request from the same chat/session was submitted. "
        "Continuing with the latest task `new`."
    )
    writes = {"count": 0}

    def _write_status(_task_id: str, _message: str) -> None:
        writes["count"] += 1
        if writes["count"] == 2:
            raise SupersededTaskError(superseded)

    monkeypatch.setattr(handler, "_write_task_status", _write_status, raising=True)

    attempts = {"count": 0}

    def _transient_error() -> object:
        attempts["count"] += 1
        raise RuntimeError("temporary telegram API timeout")

    with pytest.raises(SupersededTaskError, match=r"latest task `new`"):
        handler._run_openclaw_telegram_verification_step(
            "old",
            "Verifying Telegram bot token with Telegram API",
            _transient_error,
            retries=3,
        )

    assert attempts["count"] == 1
    assert writes["count"] == 2


def test_run_openclaw_telegram_verification_step_aborts_when_superseded_on_later_attempt(
    tmp_path: Path, monkeypatch
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr(handler, "_raise_if_task_superseded", lambda _task_id: None, raising=True)

    superseded = (
        "Skipped this setup because a newer request from the same chat/session was submitted. "
        "Continuing with the latest task `new`."
    )
    attempts = {"count": 0}

    def _transient_then_superseded() -> object:
        attempts["count"] += 1
        if attempts["count"] == 1:
            raise RuntimeError("temporary telegram API timeout")
        raise SupersededTaskError(superseded)

    with pytest.raises(SupersededTaskError, match=r"latest task `new`"):
        handler._run_openclaw_telegram_verification_step(
            "old",
            "Verifying Telegram bot token with Telegram API",
            _transient_then_superseded,
            retries=3,
        )

    assert attempts["count"] == 2
    assert any("Verifying Telegram bot token with Telegram API (attempt 1/3)" in line for line in status_updates)
    assert any("failed, retrying: temporary telegram API timeout" in line for line in status_updates)
    assert any("Verifying Telegram bot token with Telegram API (attempt 2/3)" in line for line in status_updates)
    assert not any("attempt 3/3" in line for line in status_updates)


def test_run_openclaw_telegram_verification_step_retry_uses_exception_class_for_blank_errors(
    tmp_path: Path, monkeypatch
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _blank_error_then_success() -> object:
        attempts["count"] += 1
        if attempts["count"] == 1:
            raise RuntimeError()
        return {"ok": True}

    result = handler._run_openclaw_telegram_verification_step(
        "task-verify",
        "Verifying Telegram bot token with Telegram API",
        _blank_error_then_success,
        retries=3,
    )

    assert result == {"ok": True}
    assert attempts["count"] == 2
    assert any("failed, retrying: RuntimeError" in line for line in status_updates)


def test_run_openclaw_setup_command_uses_current_module_path_for_child_commands(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    captured: dict[str, object] = {}
    monkeypatch.setenv("PYTHONPATH", "/tmp/other-armorer")

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        captured["cmd"] = list(cmd)
        captured["env"] = dict(env or {})
        return subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr="")

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

    result = handler._run_openclaw_setup_command(
        ["run", "openclaw", "status"],
        task_id="task-verify",
        label="Verifying OpenClaw status",
    )

    assert captured["cmd"][:3] == [sys.executable, "-m", "armorer.main"]
    assert captured["cmd"][3:] == ["run", "openclaw", "status"]
    expected_root = str(Path(__file__).resolve().parents[1] / "src")
    assert captured["env"]["PYTHONPATH"].split(os.pathsep)[0] == expected_root
    assert captured["env"]["ARMORER_SANDBOX"] == "0"
    assert "/tmp/other-armorer" in captured["env"]["PYTHONPATH"].split(os.pathsep)[1:]
    assert result.stdout == "ok"


def test_run_openclaw_setup_command_aborts_when_newer_matching_retry_exists(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    old_path = inbox / "old.json"
    new_path = inbox / "new.json"
    old_task = {"id": "old", "prompt": "install openclaw", "source": "cli", "timestamp": 1, "metadata": {"channel_id": "1"}}
    new_task = {"id": "new", "prompt": "install openclaw", "source": "cli", "timestamp": 2, "metadata": {"channel_id": "1"}}
    old_path.write_text(json.dumps(old_task))
    new_path.write_text(json.dumps(new_task))
    handler._register_active_task_context("old", old_path, "cli", old_task)

    with pytest.raises(SupersededTaskError, match=r"latest task `new`"):
        handler._run_openclaw_setup_command(
            ["run", "openclaw", "status"],
            task_id="old",
            label="Verifying OpenClaw status",
        )


def test_run_openclaw_setup_command_retries_after_timeout(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, env, capture_output, text, kwargs
        attempts["count"] += 1
        if attempts["count"] == 1:
            raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout)
        return subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr="")

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

    result = handler._run_openclaw_setup_command(
        ["run", "openclaw", "status"],
        task_id="task-timeout",
        label="Verifying OpenClaw status",
        retries=2,
    )

    assert attempts["count"] == 2
    assert result.stdout == "ok"
    assert any("Verifying OpenClaw status (attempt 1/2)" in line for line in status_updates)
    assert any("failed, retrying: timed out after 300s" in line for line in status_updates)


def test_run_openclaw_setup_command_raises_after_timeout_exhaustion(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, env, capture_output, text, kwargs
        attempts["count"] += 1
        raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout)

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

    with pytest.raises(RuntimeError, match=r"Verifying OpenClaw status failed: timed out after 300s"):
        handler._run_openclaw_setup_command(
            ["run", "openclaw", "status"],
            task_id="task-timeout",
            label="Verifying OpenClaw status",
            retries=2,
        )

    assert attempts["count"] == 2
    assert any("Verifying OpenClaw status (attempt 1/2)" in line for line in status_updates)
    assert any("failed, retrying: timed out after 300s" in line for line in status_updates)
    assert any("Verifying OpenClaw status (attempt 2/2)" in line for line in status_updates)


def test_run_openclaw_setup_command_uses_explicit_timeout_value_from_exception(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cmd, cwd, env, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        raise subprocess.TimeoutExpired(cmd=["armorer", "run", "openclaw", "status"], timeout=111)

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

    with pytest.raises(RuntimeError, match=r"Verifying OpenClaw status failed: timed out after 111s"):
        handler._run_openclaw_setup_command(
            ["run", "openclaw", "status"],
            task_id="task-timeout-explicit",
            label="Verifying OpenClaw status",
            retries=2,
            timeout_seconds=300,
        )

    assert attempts["count"] == 2
    assert any("failed, retrying: timed out after 111s" in line for line in status_updates)


def test_run_openclaw_setup_command_uses_exit_code_when_error_output_is_empty(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, env, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        return subprocess.CompletedProcess(cmd, 7, stdout="", stderr="")

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

    with pytest.raises(RuntimeError, match=r"Verifying OpenClaw status failed: exit code 7"):
        handler._run_openclaw_setup_command(
            ["run", "openclaw", "status"],
            task_id="task-exit-code",
            label="Verifying OpenClaw status",
            retries=2,
        )

    assert attempts["count"] == 2
    assert any("failed, retrying: exit code 7" in line for line in status_updates)


def test_run_openclaw_setup_command_retries_transient_launch_exception(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cmd, cwd, env, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        if attempts["count"] == 1:
            raise OSError("temporary setup launcher failure")
        return subprocess.CompletedProcess(["ok"], 0, stdout="ok", stderr="")

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

    result = handler._run_openclaw_setup_command(
        ["run", "openclaw", "status"],
        task_id="task-launch-retry",
        label="Verifying OpenClaw status",
        retries=2,
    )

    assert attempts["count"] == 2
    assert result.stdout == "ok"
    assert any("failed, retrying: temporary setup launcher failure" in line for line in status_updates)


def test_run_openclaw_setup_command_raises_launch_exception_after_retries(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cmd, cwd, env, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        raise FileNotFoundError(2, "No such file or directory", "python")

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

    with pytest.raises(RuntimeError, match=r"Verifying OpenClaw status failed: .*No such file or directory"):
        handler._run_openclaw_setup_command(
            ["run", "openclaw", "status"],
            task_id="task-launch-fail",
            label="Verifying OpenClaw status",
            retries=2,
        )

    assert attempts["count"] == 2
    assert any("failed, retrying:" in line and "No such file or directory" in line for line in status_updates)


def test_openclaw_compose_run_raises_timeout(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)
    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, capture_output, text, kwargs
        attempts["count"] += 1
        raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout)

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

    with pytest.raises(RuntimeError, match=r"timed out after 120s"):
        handler._openclaw_compose_run(
            compose_dir,
            "task-timeout",
            "config",
            "set",
            "channels.telegram.enabled",
            "true",
        )
    assert attempts["count"] == 2
    assert any("OpenClaw config: config set channels.telegram.enabled true (attempt 1/2)" in line for line in status_updates)
    assert any("failed, retrying: timed out after 120s" in line for line in status_updates)
    assert any("OpenClaw config: config set channels.telegram.enabled true (attempt 2/2)" in line for line in status_updates)


def test_openclaw_compose_run_uses_exit_code_when_error_output_is_empty(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        return subprocess.CompletedProcess(cmd, 9, stdout="", stderr="")

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

    with pytest.raises(RuntimeError, match=r"OpenClaw command failed \(config set channels.telegram.enabled true\): exit code 9"):
        handler._openclaw_compose_run(
            compose_dir,
            "task-compose",
            "config",
            "set",
            "channels.telegram.enabled",
            "true",
        )

    assert attempts["count"] == 2
    assert any("failed, retrying: exit code 9" in line for line in status_updates)


def test_openclaw_compose_run_redacts_gemini_key_in_command_label_when_error_output_is_empty(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    leaked_key = "AIzaSyD0123456789abcdefghijklmnopqrstuv"
    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        return subprocess.CompletedProcess(cmd, 9, stdout="", stderr="")

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

    with pytest.raises(RuntimeError, match=r"OpenClaw command failed \(config set GEMINI_API_KEY <redacted>\): exit code 9"):
        handler._openclaw_compose_run(
            compose_dir,
            "task-compose",
            "config",
            "set",
            "GEMINI_API_KEY",
            leaked_key,
        )

    assert attempts["count"] == 2
    assert any("OpenClaw config: config set GEMINI_API_KEY <redacted> (attempt 1/2)" in line for line in status_updates)
    assert any("failed, retrying: exit code 9" in line for line in status_updates)
    assert not any(leaked_key in line for line in status_updates)


def test_openclaw_compose_run_retries_transient_timeout(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, capture_output, text, kwargs
        attempts["count"] += 1
        if attempts["count"] == 1:
            raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout)
        return subprocess.CompletedProcess(cmd, 0, stdout="{}", stderr="")

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

    result = handler._openclaw_compose_run(
        compose_dir,
        "task-compose",
        "config",
        "get",
        "channels.telegram.botToken",
    )

    assert attempts["count"] == 2
    assert result.returncode == 0
    assert any("OpenClaw config: config get channels.telegram.botToken (attempt 1/2)" in line for line in status_updates)
    assert any("failed, retrying: timed out after 120s" in line for line in status_updates)


def test_openclaw_compose_run_retries_transient_nonzero_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        if attempts["count"] == 1:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="temporary compose error")
        return subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr="")

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

    result = handler._openclaw_compose_run(
        compose_dir,
        "task-compose",
        "config",
        "set",
        "channels.telegram.enabled",
        "true",
    )

    assert attempts["count"] == 2
    assert result.stdout == "ok"
    assert any("OpenClaw config: config set channels.telegram.enabled true (attempt 1/2)" in line for line in status_updates)
    assert any("failed, retrying: temporary compose error" in line for line in status_updates)


def test_openclaw_compose_run_retries_transient_launch_exception(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        if attempts["count"] == 1:
            raise OSError("temporary compose launch failure")
        return subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr="")

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

    result = handler._openclaw_compose_run(
        compose_dir,
        "task-compose",
        "config",
        "set",
        "channels.telegram.enabled",
        "true",
    )

    assert attempts["count"] == 2
    assert result.stdout == "ok"
    assert any("failed, retrying: temporary compose launch failure" in line for line in status_updates)


def test_openclaw_compose_run_raises_launch_exception_after_retries(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cmd, cwd, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        raise FileNotFoundError(2, "No such file or directory", "docker")

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

    with pytest.raises(RuntimeError, match=r"No such file or directory"):
        handler._openclaw_compose_run(
            compose_dir,
            "task-compose",
            "config",
            "set",
            "channels.telegram.enabled",
            "true",
        )

    assert attempts["count"] == 2
    assert any("failed, retrying:" in line and "No such file or directory" in line for line in status_updates)


def test_openclaw_compose_run_aborts_on_superseded_task(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    superseded = (
        "Skipped this setup because a newer request from the same chat/session was submitted. "
        "Continuing with the latest task `new`."
    )

    monkeypatch.setattr(
        handler,
        "_raise_if_task_superseded",
        lambda _task_id: (_ for _ in ()).throw(SupersededTaskError(superseded)),
        raising=True,
    )
    monkeypatch.setattr(subprocess, "run", lambda *args, **kwargs: pytest.fail("subprocess.run must not be called"))

    with pytest.raises(SupersededTaskError, match=r"latest task `new`"):
        handler._openclaw_compose_run(
            compose_dir,
            "old",
            "config",
            "set",
            "channels.telegram.enabled",
            "true",
        )


def test_openclaw_compose_run_aborts_when_superseded_during_retry_note(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(handler, "_raise_if_task_superseded", lambda _task_id: None, raising=True)
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    superseded = (
        "Skipped this setup because a newer request from the same chat/session was submitted. "
        "Continuing with the latest task `new`."
    )
    writes = {"count": 0}

    def _write_status(_task_id: str, _message: str) -> None:
        writes["count"] += 1
        if writes["count"] == 2:
            raise SupersededTaskError(superseded)

    monkeypatch.setattr(handler, "_write_task_status", _write_status, raising=True)

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="temporary compose error")

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

    with pytest.raises(SupersededTaskError, match=r"latest task `new`"):
        handler._openclaw_compose_run(
            compose_dir,
            "old",
            "config",
            "set",
            "channels.telegram.enabled",
            "true",
        )

    assert attempts["count"] == 1
    assert writes["count"] == 2


def test_openclaw_compose_run_aborts_when_superseded_before_later_attempt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr("armorer.core.time.sleep", lambda *_args, **_kwargs: None)

    superseded = (
        "Skipped this setup because a newer request from the same chat/session was submitted. "
        "Continuing with the latest task `new`."
    )
    supersede_checks = {"count": 0}

    def _raise_if_superseded(_task_id: str) -> None:
        supersede_checks["count"] += 1
        if supersede_checks["count"] >= 2:
            raise SupersededTaskError(superseded)

    monkeypatch.setattr(handler, "_raise_if_task_superseded", _raise_if_superseded, raising=True)

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="temporary compose error")

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

    with pytest.raises(SupersededTaskError, match=r"latest task `new`"):
        handler._openclaw_compose_run(
            compose_dir,
            "old",
            "config",
            "set",
            "channels.telegram.enabled",
            "true",
        )

    assert attempts["count"] == 1
    assert any("OpenClaw config: config set channels.telegram.enabled true (attempt 1/2)" in line for line in status_updates)
    assert any("failed, retrying: temporary compose error" in line for line in status_updates)
    assert not any("attempt 2/2" in line for line in status_updates)


def test_openclaw_setup_failure_superseded_does_not_leave_retry_stuck_on_old_confirm_state(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda state, task_id: (_ for _ in ()).throw(RuntimeError("boom")),
        raising=True,
    )

    superseded_message = (
        "Skipped this setup because a newer request from the same chat/session was submitted. "
        "Continuing with the latest task `oc10`."
    )
    original_write_status = handler._write_task_status

    def _fake_write_task_status(task_id: str, message: str):
        if str(message).startswith("OpenClaw setup failed:"):
            raise SupersededTaskError(superseded_message)
        return original_write_status(task_id, message)

    monkeypatch.setattr(handler, "_write_task_status", _fake_write_task_status, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    (inbox / "oc9.json").write_text(
        json.dumps({"id": "oc9", "prompt": "UI confirmation: yes", "source": "cli", "timestamp": 9, "metadata": {"channel_id": "1"}})
    )

    handler._process_inbox_task(str(inbox / "oc9.json"))
    old_payload = json.loads(json.loads((config_dir / "outbox" / "oc9.json").read_text())["response"])
    assert old_payload.get("message") == superseded_message

    (inbox / "oc10.json").write_text(
        json.dumps({"id": "oc10", "prompt": "install openclaw", "source": "cli", "timestamp": 10, "metadata": {"channel_id": "1"}})
    )

    handler._process_inbox_task(str(inbox / "oc10.json"))
    new_payload = json.loads(json.loads((config_dir / "outbox" / "oc10.json").read_text())["response"])
    assert new_payload.get("ui_action", {}).get("type") == "select"
    assert "I’ll install and configure OpenClaw entirely from chat." in new_payload.get("message", "")


def test_openclaw_setup_execute_superseded_does_not_leave_conversation_stuck(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    superseded_message = (
        "Skipped this setup because a newer request from the same chat/session was submitted. "
        "Continuing with the latest task `oc7`."
    )
    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda state, task_id: (_ for _ in ()).throw(SupersededTaskError(superseded_message)),
        raising=True,
    )

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc7.json").read_text())["response"])
    assert payload.get("message") == superseded_message
    assert payload.get("ui_action") is None

    (inbox / "oc8.json").write_text(
        json.dumps({"id": "oc8", "prompt": "install openclaw", "source": "cli", "timestamp": 8, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "oc8.json"))
    new_payload = json.loads(json.loads((config_dir / "outbox" / "oc8.json").read_text())["response"])
    assert new_payload.get("ui_action", {}).get("type") == "select"
    assert "I’ll install and configure OpenClaw entirely from chat." in new_payload.get("message", "")


def test_openclaw_setup_flow_reuses_existing_runtime_telegram_config_when_armorer_config_is_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OPENAI_API_KEY",
        "TELEGRAM_BOT_TOKEN",
        "ARMORER_TELEGRAM_ADMIN_USER_IDS",
        "ARMORER_TELEGRAM_ADMIN_CHAT_IDS",
        "ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS",
        "TELEGRAM_ALERT_CHAT_ID",
    ):
        monkeypatch.delenv(key, raising=False)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "channels": {
                    "telegram": {
                        "botToken": "123456:runtime_existing_token_value",
                        "enabled": True,
                        "groupPolicy": "allowlist",
                        "allowFrom": ["12345", "-100999"],
                        "groupAllowFrom": ["12345", "-100999"],
                    }
                },
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (compose_dir / ".env").write_text(
        "TELEGRAM_ALERT_CHAT_ID=-100777\n"
        "ARMORER_SKILL_SCAN_PATHS=existing/skills .openclaw/custom\n"
    )
    (cfg_dir / ".env").write_text(
        "OPENAI_API_KEY=existing-openai-key\n"
        "ARMORER_SKILL_SCAN_PATHS=existing/skills .openclaw/custom\n"
        "TELEGRAM_ALERT_CHAT_ID=-100777\n"
    )
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    config_values: dict[str, object] = {}

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                if isinstance(value, list):
                    out = json.dumps(value)
                else:
                    out = str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    cm = ConfigManager()
    cm.use_keyring = False
    assert "Telegram admin" in payloads["oc4"].get("message", "")
    assert "TELEGRAM_ALERT_CHAT_ID" in payloads["oc5"].get("message", "")
    assert payloads["oc8"].get("ui_action", {}).get("type") == "confirm"
    assert "- telegram: yes" in payloads["oc8"].get("message", "")
    assert cm.get_key("TELEGRAM_BOT_TOKEN") == "123456:runtime_existing_token_value"
    assert cm.get_key("ARMORER_TELEGRAM_ADMIN_USER_IDS") is None
    assert cm.get_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS") is None
    assert cm.get_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS") is None
    assert cm.get_key("TELEGRAM_ALERT_CHAT_ID") == "-100777"
    assert "Telegram channel configuration verified via Telegram API" in payloads["oc9"].get("message", "")
    assert config_values["channels.telegram.botToken"] == "123456:runtime_existing_token_value"
    assert config_values["channels.telegram.enabled"] == "true"
    assert config_values["channels.telegram.groupPolicy"] == "allowlist"
    assert config_values["channels.telegram.allowFrom"] == ["12345", "-100999"]
    assert config_values["channels.telegram.groupAllowFrom"] == ["12345", "-100999"]
    assert "TELEGRAM_ALERT_CHAT_ID=-100777" in (cfg_dir / ".env").read_text()


def test_openclaw_setup_flow_recovers_stale_container_registry_path_on_host(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OPENAI_API_KEY",
    ):
        monkeypatch.delenv(key, raising=False)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path="/app/installed/openclaw",
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Provider: unchanged (openai)" in last_payload.get("message", "")
    cfg = json.loads((cfg_dir / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "openai/existing-model"
    assert cfg["skills"]["load"]["extraDirs"] == ["skills", ".openclaw/skills"]
    assert "OPENAI_API_KEY=existing-openai-key" in (cfg_dir / ".env").read_text()


def test_openclaw_setup_flow_repairs_stale_registry_path_when_reusing_fallback_runtime(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OPENAI_API_KEY",
    ):
        monkeypatch.delenv(key, raising=False)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(tmp_path / "missing" / "openclaw"),
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            raise AssertionError("live fallback runtime should be reused without reinstall")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Warning: Armorer could not persist OpenClaw registry metadata." not in last_payload.get("message", "")
    persisted = registry.get_app("openclaw")
    assert persisted is not None
    assert persisted.install_path == str(compose_dir)
    assert [args[:4] for args in run_calls] == [
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_succeeds_when_registered_health_check_is_unavailable(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            run_commands=[
                {
                    "label": "Health check",
                    "service": "openclaw-cli",
                    "command": "doctor",
                    "category": "run",
                    "description": "Run OpenClaw health checks.",
                }
            ],
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            raise AssertionError("existing runtime should be reused without reinstall")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "doctor"]:
            raise RuntimeError("Running OpenClaw health check failed: unexpected command `doctor`")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")
    assert "doctor" not in last_payload.get("message", "").lower()
    assert [args[:4] for args in run_calls] == [
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_surfaces_registered_health_check_model_not_found_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            run_commands=[
                {
                    "label": "Health check",
                    "service": "openclaw-cli",
                    "command": "doctor",
                    "category": "run",
                    "description": "Run OpenClaw health checks.",
                }
            ],
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            raise AssertionError("existing runtime should be reused without reinstall")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "doctor"]:
            raise RuntimeError("Running OpenClaw health check failed: provider model not found")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" not in last_payload.get("message", "")
    assert "OpenClaw setup failed: Running OpenClaw health check failed: provider model not found" in last_payload.get(
        "message", ""
    )
    assert "Armorer restored the previous OpenClaw runtime state before retry." in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    assert handler._pending_decisions.get("cli:1", {}).get("step") == "confirm"
    assert [args[:4] for args in run_calls] == [
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
        ["armorer", "run", "openclaw", "--mode"],
    ]


def test_openclaw_setup_flow_repairs_existing_non_runtime_registry_path_when_reusing_fallback_runtime(
    tmp_path: Path, monkeypatch
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OPENAI_API_KEY",
    ):
        monkeypatch.delenv(key, raising=False)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ghcr.io/openclaw/openclaw:main\n"
    )
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "skills": {"load": {"extraDirs": ["skills", ".openclaw/skills"]}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")
    stale_dir = tmp_path / "stale" / "openclaw"
    stale_dir.mkdir(parents=True, exist_ok=True)
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(stale_dir),
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

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

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        run_calls.append(args)
        if args[:2] == ["armorer", "install"]:
            raise AssertionError("live fallback runtime should be reused without reinstall")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Warning: Armorer could not persist OpenClaw registry metadata." not in last_payload.get("message", "")
    persisted = registry.get_app("openclaw")
    assert persisted is not None
    assert persisted.install_path == str(compose_dir)
    assert [args[:4] for args in run_calls] == [
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "--mode"],
        ["armorer", "run", "openclaw", "status"],
        ["armorer", "run", "openclaw", "doctor"],
    ]


def test_openclaw_setup_flow_reuses_existing_install_root_env_when_armorer_config_is_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OPENAI_API_KEY",
    ):
        monkeypatch.delenv(key, raising=False)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / ".env").write_text(
        "\n".join(
            [
                "MODEL_PROVIDER=openai",
                "Armorer_MODEL=existing-root-model",
                "OPENAI_API_KEY=existing-root-openai-key",
                "ARMORER_SKILL_SCAN_PATHS=skills .openclaw/skills",
            ]
        )
        + "\n"
    )
    _materialize_skill_dirs(compose_dir, "skills", ".openclaw/skills")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Provider: unchanged (openai)" in last_payload.get("message", "")
    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "openai/existing-root-model"
    assert cfg["skills"]["load"]["extraDirs"] == ["skills", ".openclaw/skills"]
    assert "OPENAI_API_KEY=existing-root-openai-key" in (compose_dir / ".openclaw" / ".env").read_text()


def test_openclaw_setup_flow_reuses_existing_ollama_root_env_when_armorer_config_is_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OLLAMA_BASE_URL",
    ):
        monkeypatch.delenv(key, raising=False)

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / ".env").write_text(
        "\n".join(
            [
                "MODEL_PROVIDER=ollama",
                "Armorer_MODEL=existing-ollama-model",
                "OLLAMA_BASE_URL=http://existing-ollama:11434",
            ]
        )
        + "\n"
    )
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Provider: unchanged (ollama)" in last_payload.get("message", "")
    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["agents"]["defaults"]["model"]["primary"] == "ollama/existing-ollama-model"
    assert cfg["models"]["providers"]["ollama"]["baseUrl"] == "http://existing-ollama:11434"
    assert cfg["models"]["providers"]["ollama"]["apiKey"] == "ollama-local"
    assert "OLLAMA_API_KEY=ollama-local" in (compose_dir / ".openclaw" / ".env").read_text()


def test_openclaw_setup_flow_base_url_skip_keeps_existing_ollama_base_url(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("OLLAMA_BASE_URL", "http://existing-ollama:11434")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: ollama"),
        ("oc3", "UI input: llama3.2"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("OLLAMA_BASE_URL") == "http://existing-ollama:11434"


def test_openclaw_setup_flow_alert_chat_skip_keeps_existing_alert_chat_id(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    handler.config_manager.set_key("OPENAI_API_KEY", "existing-openai-key")
    handler.config_manager.set_key("TELEGRAM_ALERT_CHAT_ID", "-100existing")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            return subprocess.CompletedProcess(args, 0, stdout="true" if args[8] == "channels.telegram.enabled" else "", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc6", "UI input: 12345"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI input: skip"),
        ("oc10", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("TELEGRAM_ALERT_CHAT_ID") == "-100existing"


def test_openclaw_setup_flow_telegram_token_skip_reuses_existing_channel_config(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    handler.config_manager.set_key("TELEGRAM_BOT_TOKEN", "123456:existing_token_value")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_USER_IDS", "12345,-100999")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS", "12345,-100999")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS", "false")
    handler.config_manager.set_key("TELEGRAM_ALERT_CHAT_ID", "-100777")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    config_values: dict[str, object] = {}
    status_calls: list[str] = []

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            status_calls.append("run-background")
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            status_calls.append("status")
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                if isinstance(value, list):
                    out = json.dumps(value)
                else:
                    out = str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("TELEGRAM_BOT_TOKEN") == "123456:existing_token_value"
    assert cm.get_key("TELEGRAM_ALERT_CHAT_ID") == "-100777"
    assert "Telegram admin" in payloads["oc4"].get("message", "")
    assert "TELEGRAM_ALERT_CHAT_ID" in payloads["oc5"].get("message", "")
    assert payloads["oc8"].get("ui_action", {}).get("type") == "confirm"
    assert "Telegram channel configuration verified via Telegram API" in payloads["oc9"].get("message", "")
    assert status_calls == ["run-background", "run-background", "status"]
    assert config_values["channels.telegram.botToken"] == "123456:existing_token_value"
    assert config_values["channels.telegram.enabled"] == "true"
    assert config_values["channels.telegram.groupPolicy"] == "allowlist"
    assert config_values["channels.telegram.allowFrom"] == ["12345", "-100999"]
    assert config_values["channels.telegram.groupAllowFrom"] == ["12345", "-100999"]


def test_openclaw_setup_flow_new_telegram_token_skip_keeps_existing_channel_restrictions(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    handler.config_manager.set_key("TELEGRAM_BOT_TOKEN", "123456:existing_token_value")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_USER_IDS", "12345,-100999")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS", "12345,-100999")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS", "false")
    handler.config_manager.set_key("TELEGRAM_ALERT_CHAT_ID", "-100777")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    config_values: dict[str, object] = {}
    status_calls: list[str] = []

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            status_calls.append("run-background")
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            status_calls.append("status")
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                if isinstance(value, list):
                    out = json.dumps(value)
                else:
                    out = str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:new_token_value_for_openclaw"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    cm = ConfigManager()
    cm.use_keyring = False
    assert "skip` to keep the current settings" in payloads["oc4"].get("message", "")
    assert "skip` to keep the current setting" in payloads["oc5"].get("message", "")
    assert payloads["oc8"].get("ui_action", {}).get("type") == "confirm"
    assert "- telegram access: allowlist (12345, -100999)" in payloads["oc8"].get("message", "")
    assert "- alert chat: -100777" in payloads["oc8"].get("message", "")
    assert "Telegram channel configuration verified via Telegram API" in payloads["oc9"].get("message", "")
    assert status_calls == ["run-background", "run-background", "status"]
    assert cm.get_key("TELEGRAM_BOT_TOKEN") == "123456:new_token_value_for_openclaw"
    assert cm.get_key("TELEGRAM_ALERT_CHAT_ID") == "-100777"
    assert config_values["channels.telegram.botToken"] == "123456:new_token_value_for_openclaw"
    assert config_values["channels.telegram.enabled"] == "true"
    assert config_values["channels.telegram.groupPolicy"] == "allowlist"
    assert config_values["channels.telegram.allowFrom"] == ["12345", "-100999"]
    assert config_values["channels.telegram.groupAllowFrom"] == ["12345", "-100999"]


def test_openclaw_setup_flow_provider_switch_requires_new_provider_key_instead_of_reusing_old_runtime_key(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "ollama/existing-model"}}},
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OLLAMA_API_KEY=ollama-local\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    def _unexpected_run(cmd, **kwargs):
        raise AssertionError(f"Setup should not execute while OPENAI_API_KEY is still missing: {cmd}")

    monkeypatch.setattr(subprocess, "run", _unexpected_run)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI confirmation: yes"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc7"].get("ui_action", {}).get("type") == "confirm"
    assert payloads["oc8"].get("ui_action", {}).get("type") == "text"
    assert "OPENAI_API_KEY" in payloads["oc8"].get("message", "")
    assert "skip` will not work until the key is configured" in payloads["oc8"].get("message", "")


def test_openclaw_setup_flow_skill_paths_skip_keeps_existing_scan_paths(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    handler.config_manager.set_key("ARMORER_SKILL_SCAN_PATHS", "existing/skills .openclaw/custom")
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)
    (compose_dir / "existing" / "skills").mkdir(parents=True, exist_ok=True)
    (compose_dir / ".openclaw" / "custom").mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    cm = ConfigManager()
    cm.use_keyring = False
    assert cm.get_key("ARMORER_SKILL_SCAN_PATHS") == "existing/skills .openclaw/custom"
    cfg = json.loads((compose_dir / ".openclaw" / "openclaw.json").read_text())
    assert cfg["skills"]["load"]["extraDirs"] == ["existing/skills", ".openclaw/custom"]


def test_openclaw_setup_flow_armory_skills_skip_keeps_existing_injected_skills(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "mcpServers": {
                    "existing-skill": {
                        "command": "npx",
                        "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
                    }
                }
            },
            indent=2,
        )
        + "\n"
    )
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            injected_skills=["existing-skill"],
        )
    )

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    cfg = json.loads((cfg_dir / "openclaw.json").read_text())
    assert "existing-skill" in cfg.get("mcpServers", {})
    app_meta = registry.get_app("openclaw")
    assert app_meta is not None
    assert app_meta.injected_skills == ["existing-skill"]


def test_openclaw_setup_flow_clear_reused_optional_settings(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    handler.config_manager.set_key("TELEGRAM_BOT_TOKEN", "123456:existing_token_value")
    handler.config_manager.set_key("TELEGRAM_ALERT_CHAT_ID", "-100777")
    handler.config_manager.set_key("ARMORER_SKILL_SCAN_PATHS", "existing/skills .openclaw/custom")

    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "channels": {
                    "telegram": {
                        "botToken": "123456:existing_token_value",
                        "enabled": True,
                        "groupPolicy": "allowlist",
                        "allowFrom": ["12345", "-100999"],
                        "groupAllowFrom": ["12345", "-100999"],
                    }
                },
                "skills": {"load": {"extraDirs": ["existing/skills", ".openclaw/custom"]}},
                "mcpServers": {
                    "existing-skill": {
                        "command": "npx",
                        "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
                    },
                    "custom-remote": {
                        "command": "npx",
                        "args": ["-y", "custom-remote"],
                    },
                },
            },
            indent=2,
        )
        + "\n"
    )
    (compose_dir / ".env").write_text("TELEGRAM_ALERT_CHAT_ID=-100777\n")
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            injected_skills=["existing-skill"],
        )
    )

    config_values: dict[str, object] = {
        "channels.telegram.botToken": "123456:existing_token_value",
        "channels.telegram.enabled": "true",
        "channels.telegram.groupPolicy": "allowlist",
        "channels.telegram.allowFrom": ["12345", "-100999"],
        "channels.telegram.groupAllowFrom": ["12345", "-100999"],
    }
    status_calls: list[str] = []

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            status_calls.append("run-background")
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            status_calls.append("status")
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            key = args[8]
            if op == "set":
                value = args[9]
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                value = config_values.get(key, "")
                if isinstance(value, list):
                    return subprocess.CompletedProcess(args, 0, stdout=json.dumps(value), stderr="")
                return subprocess.CompletedProcess(args, 0, stdout=str(value), stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: clear"),
        ("oc6", "UI input: clear"),
        ("oc7", "UI input: clear"),
        ("oc8", "UI input: clear"),
        ("oc9", "UI confirmation: yes"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    cm = ConfigManager()
    cm.use_keyring = False
    assert "clear` to remove the current allowlist" in payloads["oc4"].get("message", "")
    assert "clear` to remove it" in payloads["oc5"].get("message", "")
    assert "clear` to remove them" in payloads["oc6"].get("message", "")
    assert "clear` to reset to the default scan paths" in payloads["oc7"].get("message", "")
    assert payloads["oc8"].get("ui_action", {}).get("type") == "confirm"
    assert "- telegram access: open" in payloads["oc8"].get("message", "")
    assert "- alert chat: none" in payloads["oc8"].get("message", "")
    assert "- armory skills: clear (existing-skill)" in payloads["oc8"].get("message", "")
    assert "- skill paths: default (clear extra dirs)" in payloads["oc8"].get("message", "")
    assert "Telegram bot token verified via Telegram API" in payloads["oc9"].get("message", "")
    assert "OpenClaw Telegram channel is enabled with no allowlisted chats." in payloads["oc9"].get("message", "")
    assert "Alert chat cleared." in payloads["oc9"].get("message", "")
    assert "Injected Armory skills cleared: existing-skill" in payloads["oc9"].get("message", "")
    assert "OpenClaw extra skill directories cleared; default scan paths remain." in payloads["oc9"].get("message", "")
    assert status_calls == ["run-background", "run-background", "status"]
    assert config_values["channels.telegram.groupPolicy"] == "open"
    assert config_values["channels.telegram.allowFrom"] == []
    assert config_values["channels.telegram.groupAllowFrom"] == []
    assert cm.get_key("TELEGRAM_ALERT_CHAT_ID") is None
    assert cm.get_key("ARMORER_SKILL_SCAN_PATHS") is None

    cfg = json.loads((cfg_dir / "openclaw.json").read_text())
    assert cfg["channels"]["telegram"]["botToken"] == "123456:existing_token_value"
    assert "extraDirs" not in cfg.get("skills", {}).get("load", {})
    assert "existing-skill" not in cfg.get("mcpServers", {})
    assert "custom-remote" in cfg.get("mcpServers", {})
    assert "TELEGRAM_ALERT_CHAT_ID" not in (compose_dir / ".env").read_text()
    assert "ARMORER_SKILL_SCAN_PATHS" not in (compose_dir / ".env").read_text()
    assert "ARMORER_SKILL_SCAN_PATHS" not in (cfg_dir / ".env").read_text()
    app_meta = registry.get_app("openclaw")
    assert app_meta is not None
    assert app_meta.injected_skills == []


def test_openclaw_setup_flow_armory_skills_skip_reapplies_existing_skills_after_reinstall(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "mcpServers": {
                    "existing-skill": {
                        "command": "npx",
                        "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
                    }
                },
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
            injected_skills=["existing-skill"],
        )
    )

    from armorer import core as core_mod

    existing_skill = SimpleNamespace(
        name="existing-skill",
        config={"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]},
    )

    class _FakeSkillManager:
        def list_skills(self):
            return [existing_skill]

        def get_skill(self, name: str):
            return existing_skill if name == "existing-skill" else None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            (cfg_dir / "openclaw.json").write_text("{}\n")
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    cfg = json.loads((cfg_dir / "openclaw.json").read_text())
    assert "existing-skill" in cfg.get("mcpServers", {})
    assert "Injected Armory skills: existing-skill" in last_payload.get("message", "")
    app_meta = registry.get_app("openclaw")
    assert app_meta is not None
    assert app_meta.injected_skills == ["existing-skill"]


def test_openclaw_setup_flow_recovers_existing_skills_from_runtime_when_registry_metadata_is_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    cfg_dir = compose_dir / ".openclaw"
    cfg_dir.mkdir(parents=True, exist_ok=True)
    (cfg_dir / "openclaw.json").write_text(
        json.dumps(
            {
                "agents": {"defaults": {"model": {"primary": "openai/existing-model"}}},
                "mcpServers": {
                    "existing-skill": {
                        "command": "npx",
                        "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
                    }
                },
            },
            indent=2,
        )
        + "\n"
    )
    (cfg_dir / ".env").write_text("OPENAI_API_KEY=existing-openai-key\n")
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(compose_dir),
            agent_id="openclaw",
        )
    )

    from armorer import core as core_mod

    existing_skill = SimpleNamespace(
        name="existing-skill",
        config={"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]},
    )

    class _FakeSkillManager:
        def list_skills(self):
            return [existing_skill]

        def get_skill(self, name: str):
            return existing_skill if name == "existing-skill" else None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            (cfg_dir / "openclaw.json").write_text("{}\n")
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "- armory skills: existing-skill" in payloads["oc6"].get("message", "")
    cfg = json.loads((cfg_dir / "openclaw.json").read_text())
    assert "existing-skill" in cfg.get("mcpServers", {})
    assert "Injected Armory skills: existing-skill" in payloads["oc7"].get("message", "")
    app_meta = registry.get_app("openclaw")
    assert app_meta is not None
    assert app_meta.injected_skills == ["existing-skill"]


def test_openclaw_setup_flow_skipping_telegram_token_skips_channel_followups(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
    ]

    payloads: list[dict] = []
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads.append(json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"]))

    token_prompt = payloads[-2]
    followup_prompt = payloads[-1]

    assert "TELEGRAM_BOT_TOKEN" in token_prompt.get("message", "")
    assert "Armory skill names" in followup_prompt.get("message", "")
    assert "Telegram admin" not in followup_prompt.get("message", "")


def test_openclaw_setup_flow_confirm_no_cancels(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: no"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert last_payload.get("message") == "Cancelled OpenClaw setup."


def test_openclaw_setup_flow_confirm_never_mind_cancels(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "never mind for now"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert last_payload.get("message") == "Cancelled OpenClaw setup."
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid) is None


def test_pending_openclaw_confirm_step_routes_unrelated_question_to_llm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "what agents are currently running?"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert last_payload.get("message") == "llm_answer"
    assert "what agents are currently running?" in captured.get("user_input", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "confirm"


def test_openclaw_setup_focus_switch_no_preserves_pending_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "tell me a joke"),
        ("oc5", "UI confirmation: no"),
        ("oc6", "UI input: sk-test-openai"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "confirm"
    assert "switch topic" in payloads["oc4"].get("message", "").lower()
    assert "Keeping focus on assistant setup/configuration" in payloads["oc5"].get("message", "")
    assert "TELEGRAM_BOT_TOKEN" in payloads["oc6"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "telegram_token"


def test_openclaw_setup_focus_switch_yes_routes_prompt_and_allows_resume(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "tell me a joke"),
        ("oc5", "UI confirmation: yes"),
        ("oc6", "UI input: sk-test-openai"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "confirm"
    assert payloads["oc5"].get("message") == "llm_answer"
    assert captured.get("user_input") == "tell me a joke"
    assert "TELEGRAM_BOT_TOKEN" in payloads["oc6"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "telegram_token"


def test_openclaw_setup_focus_switch_then_natural_cancel_phrase_cancels(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "tell me a joke"),
        ("oc5", "UI confirmation: yes"),
        ("oc6", "cancel please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "confirm"
    assert payloads["oc5"].get("message") == "llm_answer"
    assert captured.get("user_input") == "tell me a joke"
    assert payloads["oc6"].get("message") == "Cancelled OpenClaw setup."
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid) is None


def test_openclaw_setup_focus_switch_then_never_mind_phrase_cancels(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "tell me a joke"),
        ("oc5", "UI confirmation: yes"),
        ("oc6", "never mind for now"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "confirm"
    assert payloads["oc5"].get("message") == "llm_answer"
    assert captured.get("user_input") == "tell me a joke"
    assert payloads["oc6"].get("message") == "Cancelled OpenClaw setup."
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid) is None


def test_openclaw_setup_explicit_switch_topic_clears_focus_without_consuming_confirm(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "switch topic"),
        ("oc5", "UI confirmation: yes"),
        ("oc6", "tell me a joke"),
        ("oc7", "UI input: sk-test-openai"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "confirm"
    assert payloads["oc5"].get("message") == "Assistant setup/configuration focus cleared. Ask your next question."
    assert payloads["oc6"].get("message") == "llm_answer"
    assert captured.get("user_input") == "tell me a joke"
    assert "TELEGRAM_BOT_TOKEN" in payloads["oc7"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "telegram_token"


def test_openclaw_setup_flow_reports_failure_and_leaves_status_visible(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 1, stdout="", stderr="install exploded")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(subprocess, "run", _fake_run)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert "No further changes were applied after the failure." in last_payload.get("message", "")
    assert "removed the partial OpenClaw install before retry" not in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "confirm"
    assert not (config_dir / "status" / "oc7.json").exists()


def test_openclaw_setup_flow_starts_for_open_claw_spaced_name(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "please install open claw for me",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payload.get("message", "")


def test_openclaw_setup_flow_reuses_single_existing_non_default_alias_on_start(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "install openclaw",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert "existing OpenClaw instance `prindleclaw`" in payload.get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("alias") == "prindleclaw"


def test_openclaw_setup_flow_prompts_for_alias_when_multiple_non_default_aliases_exist(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "install openclaw",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw instance alias"
    assert set(payload.get("ui_action", {}).get("options") or []) == {"prindleclaw", "teamclaw", "openclaw"}
    assert "multiple OpenClaw instances" in payload.get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "alias"


def test_openclaw_setup_flow_alias_selection_advances_to_provider(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: teamclaw"),
    ]
    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps(
                {
                    "id": task_id,
                    "prompt": prompt,
                    "source": "cli",
                    "timestamp": idx,
                    "metadata": {"channel_id": "1"},
                }
            )
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc2"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "instance `teamclaw`" in payloads["oc2"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("alias") == "teamclaw"


def test_openclaw_setup_flow_alias_selection_accepts_natural_phrase(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "use teamclaw please"),
    ]
    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps(
                {
                    "id": task_id,
                    "prompt": prompt,
                    "source": "cli",
                    "timestamp": idx,
                    "metadata": {"channel_id": "1"},
                }
            )
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc2"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "instance `teamclaw`" in payloads["oc2"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("alias") == "teamclaw"


def test_openclaw_setup_flow_alias_selection_accepts_spaced_open_claw_phrase(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "choose open claw please"),
    ]
    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps(
                {
                    "id": task_id,
                    "prompt": prompt,
                    "source": "cli",
                    "timestamp": idx,
                    "metadata": {"channel_id": "1"},
                }
            )
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc2"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payloads["oc2"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_flow_alias_selection_accepts_default_keyword(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "use default"),
    ]
    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps(
                {
                    "id": task_id,
                    "prompt": prompt,
                    "source": "cli",
                    "timestamp": idx,
                    "metadata": {"channel_id": "1"},
                }
            )
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc2"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payloads["oc2"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_flow_alias_selection_accepts_new_instance_keyword(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "new instance"),
    ]
    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps(
                {
                    "id": task_id,
                    "prompt": prompt,
                    "source": "cli",
                    "timestamp": idx,
                    "metadata": {"channel_id": "1"},
                }
            )
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc2"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payloads["oc2"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_flow_alias_selection_accepts_new_instance_with_polite_suffix(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "new instance please"),
    ]
    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps(
                {
                    "id": task_id,
                    "prompt": prompt,
                    "source": "cli",
                    "timestamp": idx,
                    "metadata": {"channel_id": "1"},
                }
            )
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc2"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payloads["oc2"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_flow_start_infers_existing_alias_from_initial_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "install openclaw for teamclaw",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "instance `teamclaw`" in payload.get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "provider"
    assert pending.get("state", {}).get("alias") == "teamclaw"


def test_openclaw_setup_flow_start_infers_default_alias_from_initial_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "install openclaw as default alias",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payload.get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "provider"
    assert pending.get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_flow_start_infers_default_alias_from_new_instance_prompt(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "install openclaw as a new instance",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payload.get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "provider"
    assert pending.get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_flow_start_does_not_infer_default_alias_from_default_provider_phrase(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    registry = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="prindleclaw",
            install_path=str(tmp_path / "installed" / "prindleclaw"),
            agent_id="openclaw",
        )
    )
    registry.add_app(
        AppMetadata(
            name="teamclaw",
            install_path=str(tmp_path / "installed" / "teamclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "install openclaw and use default provider",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw instance alias"
    assert "multiple OpenClaw instances" in payload.get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "alias"
    assert "alias" not in (pending.get("state") or {})


def test_openclaw_setup_flow_sets_focus_on_start_for_plain_text_followups(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install open claw"),
        ("oc2", "openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "tell me a joke"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc1"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc3"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc4"].get("ui_action", {}).get("type") == "confirm"
    assert "switch topic" in payloads["oc4"].get("message", "").lower()
    assert captured.get("user_input", "") != "tell me a joke"


def test_openclaw_setup_flow_accepts_google_alias_as_provider_plain_reply(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install open claw"),
        ("oc2", "google"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc1"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc2"].get("ui_action", {}).get("message") == "Enter model name"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "model"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("provider") == "gemini"


def test_openclaw_setup_flow_accepts_natural_provider_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "openai please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc1"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc2"].get("ui_action", {}).get("message") == "Enter model name"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "model"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("provider") == "openai"


def test_openclaw_setup_flow_reprompts_when_provider_reply_mentions_multiple_providers(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "openai or ollama"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc2"].get("ui_action", {}).get("type") == "select"
    assert "Choose one of: ollama, openai, anthropic, openrouter, gemini, skip." in payloads["oc2"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider"


def test_openclaw_setup_flow_accepts_plain_model_reply_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "openai"),
        ("oc3", "gpt-4.1-mini"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc1"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("type") == "text"
    assert "model name" in payloads["oc2"].get("message", "").lower()
    assert payloads["oc3"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc3"].get("ui_action", {}).get("sensitive") is True
    assert "OPENAI_API_KEY" in payloads["oc3"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider_key"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("model") == "gpt-4.1-mini"


def test_openclaw_setup_flow_accepts_natural_model_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "openai"),
        ("oc3", "use gpt-4.1-mini please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc1"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("type") == "text"
    assert "model name" in payloads["oc2"].get("message", "").lower()
    assert payloads["oc3"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc3"].get("ui_action", {}).get("sensitive") is True
    assert "OPENAI_API_KEY" in payloads["oc3"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "provider_key"
    assert handler._pending_decisions.get(cid, {}).get("state", {}).get("model") == "gpt-4.1-mini"


def test_openclaw_setup_flow_accepts_natural_provider_key_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "openai"),
        ("oc3", "gpt-4.1-mini"),
        ("oc4", "key is sk-test-openai please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc4"].get("ui_action", {}).get("sensitive") is True
    assert "TELEGRAM_BOT_TOKEN" in payloads["oc4"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "telegram_token"
    assert pending.get("state", {}).get("provider_key_value") == "sk-test-openai"


def test_openclaw_setup_flow_accepts_natural_telegram_admin_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "123456:abcDEF_token_sample_value"),
        ("oc5", "admins are 12345, -100999 please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc5"].get("ui_action", {}).get("type") == "text"
    assert "TELEGRAM_ALERT_CHAT_ID" in payloads["oc5"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "alert_chat"
    assert pending.get("state", {}).get("telegram_admin_ids") == "12345,-100999"


def test_openclaw_setup_flow_accepts_natural_alert_chat_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "123456:abcDEF_token_sample_value"),
        ("oc5", "skip"),
        ("oc6", "alert chat is -100777 please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc6"].get("ui_action", {}).get("type") == "text"
    assert "Armory skill names" in payloads["oc6"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "armory_skills"
    assert pending.get("state", {}).get("alert_chat_id") == "-100777"


def test_openclaw_setup_flow_accepts_natural_base_url_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "ollama"),
        ("oc3", "llama3.2"),
        ("oc4", "base url is http://127.0.0.1:11434 please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc4"].get("ui_action", {}).get("sensitive") is True
    assert "TELEGRAM_BOT_TOKEN" in payloads["oc4"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "telegram_token"
    assert pending.get("state", {}).get("base_url") == "http://127.0.0.1:11434"


def test_openclaw_setup_flow_reprompts_for_invalid_base_url_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "ollama"),
        ("oc3", "llama3.2"),
        ("oc4", "base url for local ollama please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert "Enter a valid Ollama base URL" in payloads["oc4"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "base_url"


def test_openclaw_setup_flow_accepts_natural_telegram_token_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "telegram token is 123456:abcDEF_token_sample_value please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert "Telegram admin IDs" in payloads["oc4"].get("ui_action", {}).get("message", "")
    assert "default" not in (payloads["oc4"].get("ui_action", {}) or {})
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "telegram_admins"
    assert pending.get("state", {}).get("telegram_token") == "123456:abcDEF_token_sample_value"


def test_openclaw_setup_flow_reprompts_for_invalid_telegram_token_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "telegram token for local bot please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert payloads["oc4"].get("ui_action", {}).get("sensitive") is True
    assert "Telegram bot tokens usually look like" in payloads["oc4"].get("message", "")
    assert "default" not in (payloads["oc4"].get("ui_action", {}) or {})
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "telegram_token"


def test_openclaw_setup_flow_telegram_question_does_not_hijack_to_generic_telegram_setup(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "can you help me setup telegram?"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert "@BotFather" in payloads["oc4"].get("message", "")
    assert "Armorer-managed assistants" not in payloads["oc4"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("type") == "openclaw_setup"
    assert pending.get("step") == "telegram_token"


def test_openclaw_setup_flow_telegram_phrase_does_not_hijack_to_generic_telegram_setup(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "setup telegram please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert "Telegram bot tokens usually look like" in payloads["oc4"].get("message", "")
    assert "Armorer-managed assistants" not in payloads["oc4"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("type") == "openclaw_setup"
    assert pending.get("step") == "telegram_token"


def test_openclaw_setup_flow_telegram_phrase_does_not_hijack_to_generic_telegram_setup_at_provider_step(
    tmp_path: Path, monkeypatch
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "setup telegram please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc2"].get("ui_action", {}).get("type") == "select"
    assert "Choose one of:" in payloads["oc2"].get("message", "")
    assert "Armorer-managed assistants" not in payloads["oc2"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("type") == "openclaw_setup"
    assert pending.get("step") == "provider"


def test_openclaw_setup_flow_accepts_env_style_telegram_token_value_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "TELEGRAM_BOT_TOKEN=123456:abcDEF_token_sample_value"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert "Telegram admin IDs" in payloads["oc4"].get("ui_action", {}).get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "telegram_admins"
    assert pending.get("state", {}).get("telegram_token") == "123456:abcDEF_token_sample_value"


def test_openclaw_setup_flow_accepts_quoted_env_style_telegram_token_value_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", 'TELEGRAM_BOT_TOKEN = "123456:abcDEF_token_sample_value"'),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert "Telegram admin IDs" in payloads["oc4"].get("ui_action", {}).get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "telegram_admins"
    assert pending.get("state", {}).get("telegram_token") == "123456:abcDEF_token_sample_value"


def test_openclaw_setup_flow_telegram_phrase_does_not_hijack_to_generic_telegram_setup_at_alert_chat_step(
    tmp_path: Path, monkeypatch
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "123456:abcDEF_token_sample_value"),
        ("oc5", "skip"),
        ("oc6", "setup telegram please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc6"].get("ui_action", {}).get("type") == "text"
    assert "TELEGRAM_ALERT_CHAT_ID must be a numeric Telegram chat ID" in payloads["oc6"].get("message", "")
    assert "Armorer-managed assistants" not in payloads["oc6"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("type") == "openclaw_setup"
    assert pending.get("step") == "alert_chat"


def test_openclaw_setup_flow_accepts_natural_armory_skills_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "skip"),
        ("oc5", "armory skills are filesystem, brave-search please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc5"].get("ui_action", {}).get("type") == "text"
    assert "skill directories" in payloads["oc5"].get("message", "").lower()
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "skills"
    assert pending.get("state", {}).get("armory_skills") == "filesystem,brave-search"


def test_openclaw_setup_flow_reprompts_for_invalid_armory_skills_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "skip"),
        ("oc5", "armory skills for local setup please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc5"].get("ui_action", {}).get("type") == "text"
    assert "Enter comma-separated Armory skill names" in payloads["oc5"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "armory_skills"


def test_openclaw_setup_flow_accepts_natural_skill_paths_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "skip"),
        ("oc5", "skip"),
        ("oc6", "skill directories are skills, .openclaw/skills please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc6"].get("ui_action", {}).get("type") == "confirm"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "confirm"
    assert pending.get("state", {}).get("skills_paths") == "skills,.openclaw/skills"


def test_openclaw_setup_flow_reprompts_for_invalid_skill_paths_phrase_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "skip"),
        ("oc5", "skip"),
        ("oc6", "skill directories for local setup please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc6"].get("ui_action", {}).get("type") == "text"
    assert "Enter comma-separated OpenClaw skill directories" in payloads["oc6"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "skills"


def test_openclaw_setup_flow_rejects_token_like_admin_value_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "123456:abcDEF_token_sample_value"),
        ("oc5", "admins are 123456:another_token_value"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc5"].get("ui_action", {}).get("type") == "text"
    assert "Telegram admin IDs must be numeric" in payloads["oc5"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "telegram_admins"


def test_openclaw_setup_flow_rejects_token_like_alert_chat_value_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "123456:abcDEF_token_sample_value"),
        ("oc5", "skip"),
        ("oc6", "alert chat is 123456:another_token_value"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc6"].get("ui_action", {}).get("type") == "text"
    assert "TELEGRAM_ALERT_CHAT_ID must be a numeric Telegram chat ID" in payloads["oc6"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "alert_chat"


def test_openclaw_setup_flow_accepts_plain_skip_reply_with_focus_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc1"].get("ui_action", {}).get("type") == "select"
    assert payloads["oc2"].get("ui_action", {}).get("type") == "text"
    assert "model name" in payloads["oc2"].get("message", "").lower()
    assert payloads["oc3"].get("ui_action", {}).get("type") == "text"
    assert "TELEGRAM_BOT_TOKEN" in payloads["oc3"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "telegram_token"


def test_openclaw_setup_flow_treats_skip_phrase_as_skip_for_model_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "openai"),
        ("oc3", "skip for now"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc3"].get("ui_action", {}).get("type") == "text"
    assert "OPENAI_API_KEY" in payloads["oc3"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "provider_key"
    assert pending.get("state", {}).get("model") != "skip for now"


def test_openclaw_setup_flow_treats_na_phrase_as_skip_for_model_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "openai"),
        ("oc3", "n/a please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc3"].get("ui_action", {}).get("type") == "text"
    assert "OPENAI_API_KEY" in payloads["oc3"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "provider_key"
    assert pending.get("state", {}).get("model") != "n/a please"


def test_openclaw_setup_flow_treats_not_applicable_phrase_as_skip_for_provider_key_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "openai"),
        ("oc3", "gpt-4.1-mini"),
        ("oc4", "not applicable for now"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert "TELEGRAM_BOT_TOKEN" in payloads["oc4"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "telegram_token"
    assert pending.get("state", {}).get("provider_key_value") != "not applicable for now"


def test_openclaw_setup_flow_treats_skip_phrase_as_skip_for_telegram_token_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "skip please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert "Armory skill names" in payloads["oc4"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "armory_skills"


def test_openclaw_setup_flow_treats_na_phrase_as_skip_for_telegram_token_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "n/a please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc4"].get("ui_action", {}).get("type") == "text"
    assert "Armory skill names" in payloads["oc4"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("step") == "armory_skills"


def test_openclaw_setup_flow_treats_clear_phrase_as_clear_for_armory_skills_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "openai"),
        ("oc3", "skip"),
        ("oc4", "skip"),
        ("oc5", "skip"),
        ("oc6", "clear please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc6"].get("ui_action", {}).get("type") == "text"
    assert "skill directories" in payloads["oc6"].get("message", "").lower()
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "skills"
    assert pending.get("state", {}).get("clear_armory_skills") is True


def test_openclaw_setup_flow_treats_clear_phrase_as_clear_for_telegram_admins_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_USER_IDS", "12345")
    handler.config_manager.set_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS", "-100999")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "123456:abcDEF_token_sample_value"),
        ("oc5", "clear please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc5"].get("ui_action", {}).get("type") == "text"
    assert "TELEGRAM_ALERT_CHAT_ID" in payloads["oc5"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "alert_chat"
    assert pending.get("state", {}).get("clear_telegram_admin_ids") is True


def test_openclaw_setup_flow_treats_clear_phrase_as_clear_for_alert_chat_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")
    handler.config_manager.set_key("TELEGRAM_ALERT_CHAT_ID", "-100777")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "123456:abcDEF_token_sample_value"),
        ("oc5", "skip"),
        ("oc6", "clear please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc6"].get("ui_action", {}).get("type") == "text"
    assert "Armory skill names" in payloads["oc6"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "armory_skills"
    assert pending.get("state", {}).get("clear_alert_chat_id") is True


def test_openclaw_setup_flow_treats_clear_phrase_as_clear_for_skill_paths_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler, provider="openai")
    handler.config_manager.set_key("ARMORER_SKILL_SCAN_PATHS", "existing/skills .openclaw/custom")

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "skip"),
        ("oc3", "skip"),
        ("oc4", "skip"),
        ("oc5", "skip"),
        ("oc6", "clear please"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert payloads["oc6"].get("ui_action", {}).get("type") == "confirm"
    assert "default (clear extra dirs)" in payloads["oc6"].get("message", "")
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    pending = handler._pending_decisions.get(cid, {})
    assert pending.get("step") == "confirm"
    assert pending.get("state", {}).get("clear_skill_paths") is True


def test_openclaw_setup_flow_reports_timeout_failure_and_leaves_status_visible(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    install_attempts = {"count": 0}
    seen_install_timeouts: list[int] = []

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            install_attempts["count"] += 1
            seen_install_timeouts.append(int(timeout or 0))
            raise subprocess.TimeoutExpired(cmd=args, timeout=timeout)
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert install_attempts["count"] == 2
    assert seen_install_timeouts == [1200, 1200]
    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert "timed out after 1200s" in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "confirm"
    assert not (config_dir / "status" / "oc7.json").exists()


def test_openclaw_setup_confirm_accepts_plain_yes_retry_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "yes",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "yes"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_alias_accepts_plain_alias_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "alias",
            "created_at": time.time(),
            "state": {
                "available_aliases": "prindleclaw,teamclaw",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "use teamclaw please",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "instance `teamclaw`" in payload.get("message", "")
    assert captured.get("user_input", "") != "use teamclaw please"
    pending = handler._pending_decisions.get(conversation_id, {})
    assert pending.get("step") == "provider"
    assert pending.get("state", {}).get("alias") == "teamclaw"


def test_openclaw_setup_alias_accepts_spaced_open_claw_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "alias",
            "created_at": time.time(),
            "state": {
                "available_aliases": "prindleclaw,teamclaw",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "open claw",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payload.get("message", "")
    assert captured.get("user_input", "") != "open claw"
    pending = handler._pending_decisions.get(conversation_id, {})
    assert pending.get("step") == "provider"
    assert pending.get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_alias_accepts_default_keyword_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "alias",
            "created_at": time.time(),
            "state": {
                "available_aliases": "prindleclaw,teamclaw",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "default",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payload.get("message", "")
    assert captured.get("user_input", "") != "default"
    pending = handler._pending_decisions.get(conversation_id, {})
    assert pending.get("step") == "provider"
    assert pending.get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_alias_accepts_new_instance_keyword_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "alias",
            "created_at": time.time(),
            "state": {
                "available_aliases": "prindleclaw,teamclaw",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "new instance",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payload.get("message", "")
    assert captured.get("user_input", "") != "new instance"
    pending = handler._pending_decisions.get(conversation_id, {})
    assert pending.get("step") == "provider"
    assert pending.get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_alias_accepts_new_instance_with_polite_suffix_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "alias",
            "created_at": time.time(),
            "state": {
                "available_aliases": "prindleclaw,teamclaw",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "new instance please",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("ui_action", {}).get("type") == "select"
    assert payload.get("ui_action", {}).get("message") == "Choose OpenClaw provider"
    assert "install and configure OpenClaw entirely from chat" in payload.get("message", "")
    assert captured.get("user_input", "") != "new instance please"
    pending = handler._pending_decisions.get(conversation_id, {})
    assert pending.get("step") == "provider"
    assert pending.get("state", {}).get("alias") == "openclaw"


def test_openclaw_setup_confirm_accepts_plain_retry_keyword_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "retry",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "retry"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_try_again_phrase_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "try again please",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "try again please"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_natural_yes_phrase_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "yes please",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "yes please"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_proceed_phrase_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "proceed please",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "proceed please"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_go_ahead_phrase_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "go ahead with it",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "go ahead with it"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_natural_no_phrase_when_focus_is_cleared(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )
    handler._clear_focus(conversation_id)

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "no thanks",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "Cancelled OpenClaw setup."
    assert captured.get("user_input", "") != "no thanks"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_plain_retry_keyword_when_focus_is_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_focus(conversation_id, mode="assistant_setup_config", target="openclaw")
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "retry",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "retry"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_try_again_phrase_when_focus_is_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_focus(conversation_id, mode="assistant_setup_config", target="openclaw")
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "try again please",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "try again please"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_natural_yes_phrase_when_focus_is_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_focus(conversation_id, mode="assistant_setup_config", target="openclaw")
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "yes please",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "yes please"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_proceed_phrase_when_focus_is_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_focus(conversation_id, mode="assistant_setup_config", target="openclaw")
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "proceed please",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "proceed please"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_go_ahead_phrase_when_focus_is_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}
    execute_calls: list[str] = []

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    def _fake_execute(state: dict, task_id: str):
        execute_calls.append(task_id)
        return {"message": "OpenClaw retry executed"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(handler, "_execute_openclaw_setup", _fake_execute)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_focus(conversation_id, mode="assistant_setup_config", target="openclaw")
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "go ahead with it",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "OpenClaw retry executed"
    assert execute_calls == ["oc1"]
    assert captured.get("user_input", "") != "go ahead with it"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_confirm_accepts_natural_no_phrase_when_focus_is_active(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "llm_answer"}

    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_focus(conversation_id, mode="assistant_setup_config", target="openclaw")
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4.1-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-test-openai",
            },
        },
    )

    inbox = config_dir / "inbox"
    (inbox / "oc1.json").write_text(
        json.dumps(
            {
                "id": "oc1",
                "prompt": "no thanks",
                "source": "cli",
                "timestamp": 0,
                "metadata": {"channel_id": "1"},
            }
        )
    )
    handler._process_inbox_task(str(inbox / "oc1.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc1.json").read_text())["response"])
    assert payload.get("message") == "Cancelled OpenClaw setup."
    assert captured.get("user_input", "") != "no thanks"
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_flow_reports_docker_socket_hint_when_final_status_check_fails(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(
                args,
                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 args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(subprocess, "run", _fake_run)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert "Docker daemon access is required to finish OpenClaw setup." in last_payload.get("message", "")
    assert "/var/run/docker.sock" in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "confirm"


def test_openclaw_setup_flow_reports_docker_socket_hint_when_status_reports_daemon_unreachable(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(
                args,
                1,
                stdout="",
                stderr="Cannot connect to the Docker daemon. Is the docker daemon running?",
            )
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(subprocess, "run", _fake_run)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert "Docker daemon access is required to finish OpenClaw setup." in last_payload.get("message", "")
    assert "/var/run/docker.sock" in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "confirm"


def test_openclaw_setup_flow_reports_docker_cli_missing_hint_when_final_status_check_fails(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(
                args,
                1,
                stdout="",
                stderr="docker: command not found",
            )
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(subprocess, "run", _fake_run)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert "Docker CLI is required to finish OpenClaw setup." in last_payload.get("message", "")
    assert "`docker` is on PATH" in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "confirm"


def test_openclaw_setup_flow_reports_docker_cli_missing_hint_when_compose_config_write_fails(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            raise FileNotFoundError(2, "No such file or directory", "docker")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert "Docker CLI is required to finish OpenClaw setup." in last_payload.get("message", "")
    assert "`docker` is on PATH" in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "confirm"


def test_openclaw_setup_failure_hint_matches_windows_docker_cli_missing_error():
    hint = ArmorerCoreHandler._openclaw_setup_failure_hint(
        RuntimeError("[WinError 2] The system cannot find the file specified: 'docker'")
    )
    assert "Docker CLI is required to finish OpenClaw setup." in hint
    assert "Docker CLI is required to finish OpenClaw setup." in ArmorerCoreHandler._openclaw_setup_failure_hint(
        RuntimeError("'docker' is not recognized as an internal or external command")
    )
    assert (
        ArmorerCoreHandler._openclaw_setup_failure_hint(
            RuntimeError("[WinError 2] The system cannot find the file specified")
        )
        == ""
    )
    assert (
        ArmorerCoreHandler._openclaw_setup_failure_hint(
            RuntimeError("'openai' is not recognized as an internal or external command")
        )
        == ""
    )


def test_openclaw_setup_failure_hint_matches_windows_docker_daemon_unavailable_error():
    hint = ArmorerCoreHandler._openclaw_setup_failure_hint(
        RuntimeError(
            "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."
        )
    )
    assert "Docker daemon access is required to finish OpenClaw setup." in hint


def test_openclaw_setup_failure_does_not_persist_global_armorer_config(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    for key in (
        "MODEL_PROVIDER",
        "Armorer_MODEL",
        "OPENAI_API_KEY",
        "TELEGRAM_BOT_TOKEN",
        "TELEGRAM_ALERT_CHAT_ID",
        "ARMORER_SKILL_SCAN_PATHS",
    ):
        monkeypatch.delenv(key, raising=False)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 1, stdout="", stderr="install exploded")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI input: custom/skills"),
        ("oc10", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    cm = ConfigManager()
    cm.use_keyring = False
    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert cm.get_key("MODEL_PROVIDER") is None
    assert cm.get_key("Armorer_MODEL") is None
    assert cm.get_key("OPENAI_API_KEY") is None
    assert cm.get_key("TELEGRAM_BOT_TOKEN") is None
    assert cm.get_key("TELEGRAM_ALERT_CHAT_ID") is None
    assert cm.get_key("ARMORER_SKILL_SCAN_PATHS") is None


def test_openclaw_setup_failure_can_retry_from_same_confirm_step(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    install_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            install_attempts["count"] += 1
            if install_attempts["count"] <= 2:
                return subprocess.CompletedProcess(args, 1, stdout="", stderr="temporary install error")
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
        ("oc8", "UI confirmation: yes"),
    ]

    payloads = {}
    pending_steps = {}
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])
        pending_steps[task_id] = dict(handler._pending_decisions.get(cid, {}))

    assert "OpenClaw setup failed:" in payloads["oc7"].get("message", "")
    assert "Retry with the same settings" in payloads["oc7"].get("message", "")
    assert payloads["oc7"].get("ui_action", {}).get("type") == "confirm"
    assert pending_steps["oc7"].get("type") == "openclaw_setup"
    assert pending_steps["oc7"].get("step") == "confirm"
    assert install_attempts["count"] == 3
    assert "OpenClaw is installed and verified" in payloads["oc8"].get("message", "")
    assert "OpenClaw status: healthy" in payloads["oc8"].get("message", "")
    assert handler._pending_decisions.get(cid) is None


def test_openclaw_setup_failure_can_retry_from_same_confirm_step_with_try_again_phrase(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    install_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            install_attempts["count"] += 1
            if install_attempts["count"] <= 2:
                return subprocess.CompletedProcess(args, 1, stdout="", stderr="temporary install error")
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
        ("oc8", "try again please"),
    ]

    payloads = {}
    pending_steps = {}
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])
        pending_steps[task_id] = dict(handler._pending_decisions.get(cid, {}))

    assert "OpenClaw setup failed:" in payloads["oc7"].get("message", "")
    assert "Retry with the same settings" in payloads["oc7"].get("message", "")
    assert payloads["oc7"].get("ui_action", {}).get("type") == "confirm"
    assert pending_steps["oc7"].get("type") == "openclaw_setup"
    assert pending_steps["oc7"].get("step") == "confirm"
    assert install_attempts["count"] == 3
    assert "OpenClaw is installed and verified" in payloads["oc8"].get("message", "")
    assert "OpenClaw status: healthy" in payloads["oc8"].get("message", "")
    assert handler._pending_decisions.get(cid) is None


def test_openclaw_setup_failure_can_cancel_from_same_confirm_step_with_natural_no_phrase(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "agent ok"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_run, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    install_attempts = {"count": 0}

    def _fake_run_cmd(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            install_attempts["count"] += 1
            return subprocess.CompletedProcess(args, 1, stdout="", stderr="temporary install error")
        raise AssertionError(f"Unexpected command: {args}")

    monkeypatch.setattr(subprocess, "run", _fake_run_cmd)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
        ("oc8", "no thanks"),
    ]

    payloads = {}
    pending_steps = {}
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])
        pending_steps[task_id] = dict(handler._pending_decisions.get(cid, {}))

    assert "OpenClaw setup failed:" in payloads["oc7"].get("message", "")
    assert "Retry with the same settings" in payloads["oc7"].get("message", "")
    assert payloads["oc7"].get("ui_action", {}).get("type") == "confirm"
    assert pending_steps["oc7"].get("type") == "openclaw_setup"
    assert pending_steps["oc7"].get("step") == "confirm"
    assert install_attempts["count"] == 2
    assert payloads["oc8"].get("message") == "Cancelled OpenClaw setup."
    assert install_attempts["count"] == 2
    assert handler._pending_decisions.get(cid) is None

    (inbox / "oc9.json").write_text(
        json.dumps({"id": "oc9", "prompt": "tell me a joke", "source": "cli", "timestamp": 9, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "oc9.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc9.json").read_text())["response"])
    assert payload.get("message") == "agent ok"
    assert captured.get("user_input") == "tell me a joke"


def test_openclaw_setup_flow_retries_transient_install_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    install_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            install_attempts["count"] += 1
            if install_attempts["count"] == 1:
                return subprocess.CompletedProcess(args, 1, stdout="", stderr="temporary install error")
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert install_attempts["count"] == 2
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")


def test_openclaw_setup_flow_retries_transient_install_timeout(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    install_attempts = {"count": 0}
    seen_install_timeouts: list[int] = []

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            install_attempts["count"] += 1
            if install_attempts["count"] == 1:
                seen_install_timeouts.append(int(timeout or 0))
                raise subprocess.TimeoutExpired(cmd=args, timeout=timeout)
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert install_attempts["count"] == 2
    assert seen_install_timeouts == [1200]
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")


def test_openclaw_setup_flow_retries_transient_background_start_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    run_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            run_attempts["count"] += 1
            if run_attempts["count"] == 1:
                return subprocess.CompletedProcess(args, 1, stdout="", stderr="temporary startup error")
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert run_attempts["count"] == 3
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")


def test_openclaw_setup_flow_retries_transient_background_start_timeout(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    run_attempts = {"count": 0}
    seen_run_timeouts: list[int] = []

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            run_attempts["count"] += 1
            if run_attempts["count"] == 1:
                seen_run_timeouts.append(int(timeout or 0))
                raise subprocess.TimeoutExpired(cmd=args, timeout=timeout)
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert run_attempts["count"] == 3
    assert seen_run_timeouts == [300]
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")


def test_openclaw_setup_flow_reports_rollback_copy_for_fresh_install_start_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    compose_dir = tmp_path / "installed" / "openclaw"

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    start_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            compose_dir.mkdir(parents=True, exist_ok=True)
            (compose_dir / "docker-compose.yml").write_text(
                "services:\n"
                "  openclaw-cli:\n"
                "    image: ghcr.io/openclaw/openclaw:main\n"
            )
            (compose_dir / ".openclaw").mkdir(parents=True, exist_ok=True)
            (compose_dir / ".openclaw" / "openclaw.json").write_text("{}\n")
            handler.registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            start_attempts["count"] += 1
            return subprocess.CompletedProcess(args, 1, stdout="", stderr="startup exploded")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw for me"),
        ("oc2", "UI selection: openai"),
        ("oc3", "UI input: gpt-4.1-mini"),
        ("oc4", "UI input: sk-test-openai"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI input: skip"),
        ("oc10", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert start_attempts["count"] == 2
    assert "OpenClaw setup failed: Starting OpenClaw services failed: startup exploded" in last_payload.get("message", "")
    assert "Armorer removed the partial OpenClaw install before retry." in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    assert handler._pending_decisions.get("cli:1", {}).get("step") == "confirm"
    assert not compose_dir.exists()
    assert handler.registry.get_app("openclaw") is None


def test_openclaw_setup_flow_retries_transient_status_timeout(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_attempts = {"count": 0}
    seen_status_timeouts: list[int] = []

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            status_attempts["count"] += 1
            if status_attempts["count"] == 1:
                seen_status_timeouts.append(int(timeout or 0))
                raise subprocess.TimeoutExpired(cmd=args, timeout=timeout)
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert status_attempts["count"] == 2
    assert seen_status_timeouts == [180]
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")


def test_openclaw_setup_flow_retries_transient_status_verification_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            status_attempts["count"] += 1
            if status_attempts["count"] == 1:
                return subprocess.CompletedProcess(args, 1, stdout="", stderr="temporary status error")
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert status_attempts["count"] == 2
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")


def test_openclaw_setup_flow_retries_transient_telegram_api_verification_failures(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    telegram_attempts = {"get_me": 0, "get_chat": 0}

    class _FlakyTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            telegram_attempts["get_me"] += 1
            if telegram_attempts["get_me"] == 1:
                raise RuntimeError("temporary telegram API timeout")
            return {"id": 1, "username": "retry_bot"}

        def get_chat(self, chat_id: str):
            telegram_attempts["get_chat"] += 1
            if telegram_attempts["get_chat"] == 1:
                raise RuntimeError("temporary getChat failure")
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod, "TelegramGateway", _FlakyTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    config_values: dict[str, object] = {}
    status_calls: list[str] = []

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            status_calls.append("run-background")
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            status_calls.append("status")
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                out = json.dumps(value) if isinstance(value, list) else str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert telegram_attempts["get_me"] == 2
    assert telegram_attempts["get_chat"] == 2
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Telegram channel configuration verified via Telegram API" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")
    assert status_calls == ["run-background", "run-background", "status"]
    assert config_values["channels.telegram.botToken"] == "123456:abcDEF_token_sample_value"
    assert config_values["channels.telegram.enabled"] == "true"
    assert config_values["channels.telegram.groupPolicy"] == "allowlist"
    assert config_values["channels.telegram.allowFrom"] == ["12345"]
    assert config_values["channels.telegram.groupAllowFrom"] == ["12345"]


def test_openclaw_setup_flow_retries_transient_compose_set_timeout(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "retry_bot"}

        def get_chat(self, chat_id: str):
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    config_values: dict[str, object] = {}
    enabled_attempts = {"count": 0}
    seen_timeouts: list[int] = []

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                if key == "channels.telegram.enabled":
                    enabled_attempts["count"] += 1
                    if enabled_attempts["count"] == 1:
                        seen_timeouts.append(int(timeout or 0))
                        raise subprocess.TimeoutExpired(cmd=args, timeout=timeout)
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                out = json.dumps(value) if isinstance(value, list) else str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert enabled_attempts["count"] == 2
    assert seen_timeouts == [120]
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Telegram channel configuration verified via Telegram API" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")


def test_openclaw_setup_flow_retries_transient_compose_set_nonzero_failure(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "retry_bot"}

        def get_chat(self, chat_id: str):
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    config_values: dict[str, object] = {}
    enabled_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                if key == "channels.telegram.enabled":
                    enabled_attempts["count"] += 1
                    if enabled_attempts["count"] == 1:
                        return subprocess.CompletedProcess(args, 1, stdout="", stderr="temporary compose set error")
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                out = json.dumps(value) if isinstance(value, list) else str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert enabled_attempts["count"] == 2
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert "Telegram channel configuration verified via Telegram API" in last_payload.get("message", "")
    assert "OpenClaw status: healthy" in last_payload.get("message", "")


def test_openclaw_setup_flow_compose_timeout_retry_status_is_visible(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "retry_bot"}

        def get_chat(self, chat_id: str):
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    config_values: dict[str, object] = {}
    enabled_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                if key == "channels.telegram.enabled":
                    enabled_attempts["count"] += 1
                    if enabled_attempts["count"] == 1:
                        raise subprocess.TimeoutExpired(cmd=args, timeout=timeout)
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                out = json.dumps(value) if isinstance(value, list) else str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert enabled_attempts["count"] == 2
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert any("OpenClaw config: config set channels.telegram.enabled true (attempt 1/2)" in line for line in status_updates)
    assert any("failed, retrying: timed out after 120s" in line for line in status_updates)


def test_openclaw_setup_flow_compose_nonzero_retry_status_is_visible(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "retry_bot"}

        def get_chat(self, chat_id: str):
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    config_values: dict[str, object] = {}
    enabled_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del timeout
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                if key == "channels.telegram.enabled":
                    enabled_attempts["count"] += 1
                    if enabled_attempts["count"] == 1:
                        return subprocess.CompletedProcess(args, 1, stdout="", stderr="temporary compose set error")
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                out = json.dumps(value) if isinstance(value, list) else str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert enabled_attempts["count"] == 2
    assert "OpenClaw is installed and verified" in last_payload.get("message", "")
    assert any("OpenClaw config: config set channels.telegram.enabled true (attempt 1/2)" in line for line in status_updates)
    assert any("failed, retrying: temporary compose set error" in line for line in status_updates)


def test_openclaw_setup_flow_compose_retry_status_redacts_telegram_token_detail(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "retry_bot"}

        def get_chat(self, chat_id: str):
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    config_values: dict[str, object] = {}
    enabled_attempts = {"count": 0}
    leaked_token = "123456:abcDEF_token_sample_value"

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del timeout
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                if key == "channels.telegram.enabled":
                    enabled_attempts["count"] += 1
                    if enabled_attempts["count"] == 1:
                        return subprocess.CompletedProcess(args, 1, stdout="", stderr=f"temporary failure {leaked_token}")
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                out = json.dumps(value) if isinstance(value, list) else str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", f"UI input: {leaked_token}"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    assert enabled_attempts["count"] == 2
    assert any("failed, retrying:" in line for line in status_updates)
    assert any("[REDACTED_TELEGRAM_TOKEN]" in line for line in status_updates)
    assert not any(leaked_token in line for line in status_updates)


def test_openclaw_setup_flow_reports_compose_timeout_failure_and_preserves_retry_state(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    enabled_attempts = {"count": 0}
    seen_timeouts: list[int] = []

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set" and args[8] == "channels.telegram.enabled":
                enabled_attempts["count"] += 1
                seen_timeouts.append(int(timeout or 0))
                raise subprocess.TimeoutExpired(cmd=args, timeout=timeout)
            if op == "set":
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert enabled_attempts["count"] == 2
    assert seen_timeouts == [120, 120]
    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert "timed out after 120s" in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "confirm"
    assert not (config_dir / "status" / "oc9.json").exists()


def test_openclaw_setup_flow_reports_compose_nonzero_failure_and_preserves_retry_state(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    enabled_attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del timeout
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set" and args[8] == "channels.telegram.enabled":
                enabled_attempts["count"] += 1
                return subprocess.CompletedProcess(args, 1, stdout="", stderr="compose set exploded")
            if op == "set":
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert enabled_attempts["count"] == 2
    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert "OpenClaw command failed (config set channels.telegram.enabled true): compose set exploded" in last_payload.get("message", "")
    assert "Retry with the same settings" in last_payload.get("message", "")
    assert last_payload.get("ui_action", {}).get("type") == "confirm"
    cid = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(cid, {}).get("type") == "openclaw_setup"
    assert handler._pending_decisions.get(cid, {}).get("step") == "confirm"
    assert not (config_dir / "status" / "oc9.json").exists()


def test_openclaw_setup_flow_redacts_telegram_token_in_compose_failure_message(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    token_value = "123456:abcDEF_token_sample_value"

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del timeout
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            if args[7] == "set" and args[8] == "channels.telegram.botToken":
                return subprocess.CompletedProcess(args, 1, stdout="", stderr="compose set exploded")
            return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", f"UI input: {token_value}"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    last_payload = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        last_payload = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw setup failed:" in last_payload.get("message", "")
    assert "channels.telegram.botToken" in last_payload.get("message", "")
    assert token_value not in last_payload.get("message", "")


def test_run_openclaw_setup_command_retry_status_redacts_telegram_token_detail(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    leaked_token = "123456:abcDEF_token_sample_value"
    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, env, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        if attempts["count"] == 1:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr=f"temporary status error {leaked_token}")
        return subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr="")

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

    result = handler._run_openclaw_setup_command(
        ["run", "openclaw", "status"],
        task_id="t1",
        label="Verifying OpenClaw status",
        retries=2,
        timeout_seconds=10,
    )

    assert result.returncode == 0
    assert attempts["count"] == 2
    assert any("failed, retrying:" in line for line in status_updates)
    assert any("[REDACTED_TELEGRAM_TOKEN]" in line for line in status_updates)
    assert not any(leaked_token in line for line in status_updates)


def test_run_openclaw_setup_command_retry_status_redacts_sk_proj_provider_key_detail(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, env, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        if attempts["count"] == 1:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr=f"temporary status error {leaked_key}")
        return subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr="")

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

    result = handler._run_openclaw_setup_command(
        ["run", "openclaw", "status"],
        task_id="t1",
        label="Verifying OpenClaw status",
        retries=2,
        timeout_seconds=10,
    )

    assert result.returncode == 0
    assert attempts["count"] == 2
    assert any("failed, retrying:" in line for line in status_updates)
    assert any("[REDACTED_OPENAI_KEY]" in line for line in status_updates)
    assert not any(leaked_key in line for line in status_updates)


def test_run_openclaw_setup_command_retry_status_redacts_gemini_provider_key_detail(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    leaked_key = "AIzaSyD0123456789abcdefghijklmnopqrstuv"
    attempts = {"count": 0}

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del cwd, env, capture_output, text, timeout, kwargs
        attempts["count"] += 1
        if attempts["count"] == 1:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr=f"temporary status error {leaked_key}")
        return subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr="")

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

    result = handler._run_openclaw_setup_command(
        ["run", "openclaw", "status"],
        task_id="t1",
        label="Verifying OpenClaw status",
        retries=2,
        timeout_seconds=10,
    )

    assert result.returncode == 0
    assert attempts["count"] == 2
    assert any("failed, retrying:" in line for line in status_updates)
    assert any("[REDACTED_GEMINI_KEY]" in line for line in status_updates)
    assert not any(leaked_key in line for line in status_updates)


def test_openclaw_setup_flow_compose_retry_status_redacts_gemini_key_detail(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    class _FakeTelegramGateway:
        def __init__(self, bot_token=None, *args, **kwargs):
            self.bot_token = bot_token

        def get_me(self):
            return {"id": 1, "username": "retry_bot"}

        def get_chat(self, chat_id: str):
            return {"id": str(chat_id), "type": "private"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod, "TelegramGateway", _FakeTelegramGateway, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    config_values: dict[str, object] = {}
    enabled_attempts = {"count": 0}
    leaked_key = "AIzaSyD0123456789abcdefghijklmnopqrstuv"

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, timeout=None, **kwargs):
        del timeout
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        if args[:6] == ["docker", "compose", "run", "--rm", "-T", "openclaw-cli"]:
            op = args[7]
            if op == "set":
                key = args[8]
                value = args[9]
                if key == "channels.telegram.enabled":
                    enabled_attempts["count"] += 1
                    if enabled_attempts["count"] == 1:
                        return subprocess.CompletedProcess(args, 1, stdout="", stderr=f"temporary failure {leaked_key}")
                config_values[key] = json.loads(value) if value.startswith("[") else value
                return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
            if op == "get":
                key = args[8]
                value = config_values.get(key, "")
                out = json.dumps(value) if isinstance(value, list) else str(value)
                return subprocess.CompletedProcess(args, 0, stdout=out, stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: 123456:abcDEF_token_sample_value"),
        ("oc5", "UI input: 12345"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI input: skip"),
        ("oc8", "UI input: skip"),
        ("oc9", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    assert enabled_attempts["count"] == 2
    assert any("failed, retrying:" in line for line in status_updates)
    assert any("[REDACTED_GEMINI_KEY]" in line for line in status_updates)
    assert not any(leaked_key in line for line in status_updates)


def test_redact_openclaw_command_args_does_not_treat_monkey_as_secret_key(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)

    redacted = handler._redact_openclaw_command_args(("config", "set", "monkey", "banana"))

    assert redacted == "config set monkey banana"


def test_redact_openclaw_command_args_redacts_gemini_key_value(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)

    leaked_key = "AIzaSyD0123456789abcdefghijklmnopqrstuv"
    redacted = handler._redact_openclaw_command_args(("config", "set", "GEMINI_API_KEY", leaked_key))

    assert redacted == "config set GEMINI_API_KEY <redacted>"


def test_run_openclaw_telegram_verification_retry_status_redacts_telegram_token_detail(
    tmp_path: Path,
    monkeypatch,
):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    status_updates: list[str] = []
    monkeypatch.setattr(
        handler,
        "_write_task_status",
        lambda _task_id, message: status_updates.append(str(message)),
        raising=True,
    )

    leaked_token = "123456:abcDEF_token_sample_value"
    attempts = {"count": 0}

    def _unstable_call():
        attempts["count"] += 1
        if attempts["count"] == 1:
            raise RuntimeError(f"telegram API timeout {leaked_token}")
        return {"ok": True}

    result = handler._run_openclaw_telegram_verification_step(
        task_id="t1",
        label="Verifying Telegram bot token with Telegram API",
        call=_unstable_call,
        retries=2,
    )

    assert result == {"ok": True}
    assert attempts["count"] == 2
    assert any("failed, retrying:" in line for line in status_updates)
    assert any("[REDACTED_TELEGRAM_TOKEN]" in line for line in status_updates)
    assert not any(leaked_token in line for line in status_updates)


def test_openclaw_setup_flow_sanitizes_secret_like_exception_text_before_reply(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4o-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-existing-openai-token-value",
            },
        },
    )

    token_value = "123456:abcDEF_token_sample_value"

    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError(f"setup exploded with token {token_value}")),
        raising=True,
    )

    _next_prompt, payload = handler._handle_openclaw_setup_flow(
        source="cli",
        conversation_id=conversation_id,
        prompt="UI confirmation: yes",
        task_id="oc-confirm",
    )

    assert isinstance(payload, dict)
    message = str(payload.get("message") or "")
    assert "OpenClaw setup failed:" in message
    assert token_value not in message
    assert payload.get("ui_action", {}).get("type") == "confirm"


def test_openclaw_setup_flow_sanitizes_sk_proj_key_like_exception_text_before_reply(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4o-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-existing-openai-token-value",
            },
        },
    )

    key_value = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"

    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError(f"setup exploded with key {key_value}")),
        raising=True,
    )

    _next_prompt, payload = handler._handle_openclaw_setup_flow(
        source="cli",
        conversation_id=conversation_id,
        prompt="UI confirmation: yes",
        task_id="oc-confirm",
    )

    assert isinstance(payload, dict)
    message = str(payload.get("message") or "")
    assert "OpenClaw setup failed:" in message
    assert "[REDACTED_OPENAI_KEY]" in message
    assert key_value not in message
    assert payload.get("ui_action", {}).get("type") == "confirm"


def test_openclaw_setup_flow_sanitizes_gemini_key_like_exception_text_before_reply(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "gemini",
                "model": "gemini-1.5-pro",
                "provider_key_name": "GEMINI_API_KEY",
                "provider_key_value": "AIzaSyD-existing-gemini-key-value-1234567890",
            },
        },
    )

    key_value = "AIzaSyD0123456789abcdefghijklmnopqrstuv"

    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError(f"setup exploded with key {key_value}")),
        raising=True,
    )

    _next_prompt, payload = handler._handle_openclaw_setup_flow(
        source="cli",
        conversation_id=conversation_id,
        prompt="UI confirmation: yes",
        task_id="oc-confirm",
    )

    assert isinstance(payload, dict)
    message = str(payload.get("message") or "")
    assert "OpenClaw setup failed:" in message
    assert "[REDACTED_GEMINI_KEY]" in message
    assert key_value not in message
    assert payload.get("ui_action", {}).get("type") == "confirm"


def test_openclaw_setup_flow_reports_rollback_warnings_to_user(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4o-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-existing-openai-token-value",
            },
        },
    )

    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(
            OpenClawSetupExecutionError(
                "status verification failed",
                rollback_warnings=["reload services: docker unavailable"],
            )
        ),
        raising=True,
    )

    _next_prompt, payload = handler._handle_openclaw_setup_flow(
        source="cli",
        conversation_id=conversation_id,
        prompt="UI confirmation: yes",
        task_id="oc-confirm",
    )

    assert isinstance(payload, dict)
    message = str(payload.get("message") or "")
    assert "OpenClaw setup failed: status verification failed" in message
    assert "rollback reported warnings: reload services: docker unavailable" in message
    assert payload.get("ui_action", {}).get("type") == "confirm"


def test_openclaw_setup_flow_uses_custom_rollback_summary_with_warnings(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4o-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-existing-openai-token-value",
            },
        },
    )

    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(
            OpenClawSetupExecutionError(
                "starting services failed",
                rollback_summary="Armorer removed the partial OpenClaw install before retry.",
                rollback_warnings=["remove install root openclaw: permission denied"],
            )
        ),
        raising=True,
    )

    _next_prompt, payload = handler._handle_openclaw_setup_flow(
        source="cli",
        conversation_id=conversation_id,
        prompt="UI confirmation: yes",
        task_id="oc-confirm",
    )

    assert isinstance(payload, dict)
    message = str(payload.get("message") or "")
    assert "OpenClaw setup failed: starting services failed" in message
    assert (
        "Armorer removed the partial OpenClaw install before retry, "
        "but rollback reported warnings: remove install root openclaw: permission denied"
    ) in message
    assert payload.get("ui_action", {}).get("type") == "confirm"


def test_openclaw_setup_flow_uses_custom_registry_only_rollback_summary(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4o-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-existing-openai-token-value",
            },
        },
    )

    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(
            OpenClawSetupExecutionError(
                "registry recovery failed",
                rollback_summary="Armorer rolled back the partial OpenClaw setup state before retry.",
            )
        ),
        raising=True,
    )

    _next_prompt, payload = handler._handle_openclaw_setup_flow(
        source="cli",
        conversation_id=conversation_id,
        prompt="UI confirmation: yes",
        task_id="oc-confirm",
    )

    assert isinstance(payload, dict)
    message = str(payload.get("message") or "")
    assert "OpenClaw setup failed: registry recovery failed" in message
    assert "Armorer rolled back the partial OpenClaw setup state before retry." in message
    assert payload.get("ui_action", {}).get("type") == "confirm"


def test_openclaw_setup_flow_redacts_secret_like_rollback_warnings(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4o-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-existing-openai-token-value",
            },
        },
    )

    leaked_key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
    leaked_token = "123456:abcDEF_token_sample_value"
    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(
            OpenClawSetupExecutionError(
                "status verification failed",
                rollback_warnings=[f"reload services: {leaked_key} {leaked_token}"],
            )
        ),
        raising=True,
    )

    _next_prompt, payload = handler._handle_openclaw_setup_flow(
        source="cli",
        conversation_id=conversation_id,
        prompt="UI confirmation: yes",
        task_id="oc-confirm",
    )

    assert isinstance(payload, dict)
    message = str(payload.get("message") or "")
    assert "[REDACTED_OPENAI_KEY]" in message
    assert "[REDACTED_TELEGRAM_TOKEN]" in message
    assert leaked_key not in message
    assert leaked_token not in message
    assert payload.get("ui_action", {}).get("type") == "confirm"


def test_openclaw_setup_flow_reports_generic_failure_without_rollback_copy(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    handler._set_pending_decision(
        conversation_id,
        {
            "type": "openclaw_setup",
            "step": "confirm",
            "created_at": time.time(),
            "state": {
                "alias": "openclaw",
                "provider": "openai",
                "model": "gpt-4o-mini",
                "provider_key_name": "OPENAI_API_KEY",
                "provider_key_value": "sk-existing-openai-token-value",
            },
        },
    )

    monkeypatch.setattr(
        handler,
        "_execute_openclaw_setup",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("install exploded")),
        raising=True,
    )

    _next_prompt, payload = handler._handle_openclaw_setup_flow(
        source="cli",
        conversation_id=conversation_id,
        prompt="UI confirmation: yes",
        task_id="oc-confirm",
    )

    assert isinstance(payload, dict)
    message = str(payload.get("message") or "")
    assert "OpenClaw setup failed: install exploded" in message
    assert "No further changes were applied after the failure." in message
    assert "restored the previous OpenClaw runtime state before retry" not in message
    assert payload.get("ui_action", {}).get("type") == "confirm"


def test_openclaw_setup_failure_does_not_leave_conversation_stuck(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_agent_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "agent ok"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_agent_run, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 1, stdout="", stderr="install exploded")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    (inbox / "oc8.json").write_text(
        json.dumps({"id": "oc8", "prompt": "tell me a joke", "source": "cli", "timestamp": 8, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "oc8.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc8.json").read_text())["response"])
    assert payload.get("message") == "agent ok"
    assert captured.get("user_input") == "tell me a joke"


def test_openclaw_setup_failure_cancel_does_not_leave_conversation_stuck(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_agent_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "agent ok"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_agent_run, raising=True)
    monkeypatch.setattr(core_mod.time, "sleep", lambda *_args, **_kwargs: None, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            return subprocess.CompletedProcess(args, 1, stdout="", stderr="install exploded")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
        ("oc8", "cancel"),
    ]

    payloads: dict[str, dict] = {}
    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))
        payloads[task_id] = json.loads(json.loads((config_dir / "outbox" / f"{task_id}.json").read_text())["response"])

    assert "OpenClaw setup failed:" in payloads["oc7"].get("message", "")
    assert payloads["oc8"].get("message") == "Cancelled OpenClaw setup."

    (inbox / "oc9.json").write_text(
        json.dumps({"id": "oc9", "prompt": "tell me a joke", "source": "cli", "timestamp": 9, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "oc9.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc9.json").read_text())["response"])
    assert payload.get("message") == "agent ok"
    assert captured.get("user_input") == "tell me a joke"
    conversation_id = handler.guard.conversation_id(source="cli", metadata={"channel_id": "1"})
    assert handler._pending_decisions.get(conversation_id, {}).get("type") != "openclaw_setup"


def test_openclaw_setup_success_does_not_leave_conversation_stuck(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False
    _seed_existing_openclaw_runtime(handler)
    registry = AppRegistry()
    compose_dir = tmp_path / "installed" / "openclaw"
    compose_dir.mkdir(parents=True, exist_ok=True)

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_agent_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "agent ok"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_agent_run, raising=True)

    def _fake_run(cmd, cwd=None, env=None, capture_output=False, text=False, **kwargs):
        args = _normalize_setup_command_args(cmd)
        if args[:2] == ["armorer", "install"]:
            registry.add_app(
                AppMetadata(
                    name="openclaw",
                    install_path=str(compose_dir),
                    agent_id="openclaw",
                )
            )
            return subprocess.CompletedProcess(args, 0, stdout="installed", stderr="")
        if args[:5] == ["armorer", "run", "openclaw", "--mode", "background"]:
            return subprocess.CompletedProcess(args, 0, stdout="started", stderr="")
        if args[:4] == ["armorer", "run", "openclaw", "status"]:
            return subprocess.CompletedProcess(args, 0, stdout="OpenClaw status: healthy", stderr="")
        raise AssertionError(f"Unexpected command: {args}")

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

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "install openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: yes"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    (inbox / "oc8.json").write_text(
        json.dumps({"id": "oc8", "prompt": "tell me a joke", "source": "cli", "timestamp": 8, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "oc8.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc8.json").read_text())["response"])
    assert payload.get("message") == "agent ok"
    assert captured.get("user_input") == "tell me a joke"


def test_openclaw_setup_cancel_does_not_leave_conversation_stuck(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_path, monkeypatch)
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    from armorer import core as core_mod

    class _FakeSkillManager:
        def list_skills(self):
            return []

        def get_skill(self, name: str):
            return None

    captured: dict[str, str] = {}

    def _noop_init_agent(self, *args, **kwargs):
        return None

    def _fake_agent_run(self, user_input=None, *args, **kwargs):
        captured["user_input"] = str(user_input or "")
        return {"message": "agent ok"}

    monkeypatch.setattr(core_mod, "SkillManager", _FakeSkillManager, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "init_agent", _noop_init_agent, raising=True)
    monkeypatch.setattr(core_mod.BaseAgent, "run", _fake_agent_run, raising=True)

    inbox = config_dir / "inbox"
    prompts = [
        ("oc1", "setup openclaw"),
        ("oc2", "UI selection: skip"),
        ("oc3", "UI input: skip"),
        ("oc4", "UI input: skip"),
        ("oc5", "UI input: skip"),
        ("oc6", "UI input: skip"),
        ("oc7", "UI confirmation: no"),
    ]

    for idx, (task_id, prompt) in enumerate(prompts):
        (inbox / f"{task_id}.json").write_text(
            json.dumps({"id": task_id, "prompt": prompt, "source": "cli", "timestamp": idx, "metadata": {"channel_id": "1"}})
        )
        handler._process_inbox_task(str(inbox / f"{task_id}.json"))

    (inbox / "oc8.json").write_text(
        json.dumps({"id": "oc8", "prompt": "tell me a joke", "source": "cli", "timestamp": 8, "metadata": {"channel_id": "1"}})
    )
    handler._process_inbox_task(str(inbox / "oc8.json"))

    payload = json.loads(json.loads((config_dir / "outbox" / "oc8.json").read_text())["response"])
    assert payload.get("message") == "agent ok"
    assert captured.get("user_input") == "tell me a joke"


def test_guard_agent_scan_uses_openclaw_fallback_when_registry_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_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"
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

    seen: dict[str, object] = {}

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

        def scan(self):
            return SimpleNamespace(score=1.0, issues=[], critical_found=False)

    monkeypatch.setattr("armorer.scanner.VulnerabilityScanner", _FakeScanner)

    result = handler._handle_guard_command("/guard agent-scan openclaw")

    assert "Scan complete for `openclaw`" in result
    assert seen["target_dir"] == str(install_dir)


def test_guard_agent_shutdown_uses_openclaw_fallback_when_registry_is_stale(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_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 = AppRegistry()
    registry.add_app(
        AppMetadata(
            name="openclaw",
            install_path=str(tmp_path / "missing" / "openclaw"),
            agent_id="openclaw",
        )
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

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

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

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

    result = handler._handle_guard_command("/guard agent-shutdown openclaw")

    assert "Agent `openclaw` stop requested." in result
    assert calls == [(["docker", "compose", "stop"], str(install_dir))]
    assert AppRegistry().get_app("openclaw").install_path == str(install_dir)


def test_guard_agent_restart_uses_openclaw_fallback_when_registry_is_missing(tmp_path: Path, monkeypatch):
    config_dir = _setup(tmp_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"
    )
    handler = ArmorerCoreHandler(config_dir=config_dir)
    handler.config_manager.use_keyring = False

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

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

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

    result = handler._handle_guard_command("/guard agent-restart openclaw")

    assert result == "Agent `openclaw` restarted."
    assert calls == [(["docker", "compose", "restart"], str(install_dir))]
