Reformat the files touched by Tasks 1-14 of the host-agent MCP server plan. No semantic changes; pre-existing format issues in unrelated files (test_templates, test_skill_sync_wiring, 0010_skill_management, test_skill_catalog_mcp) left untouched for a separate housekeeping pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
167 lines
5.2 KiB
Python
167 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from host_agent import cli
|
|
from host_agent.instance_lock import InstanceAlreadyRunningError
|
|
from host_agent.local_account import LocalAccountStore
|
|
|
|
|
|
class _RecordingApplication:
|
|
def __init__(self) -> None:
|
|
self.ran = False
|
|
|
|
def run(self) -> None:
|
|
self.ran = True
|
|
|
|
|
|
def _base_env(tmp_path) -> dict[str, str]:
|
|
return {
|
|
"HOST_AGENT_CONTROL_PLANE_URL": "https://cloud.example",
|
|
"HOST_AGENT_LOCAL_ACCOUNT_PATH": str(tmp_path / "account.json"),
|
|
"HOST_AGENT_IDENTITY_PATH": str(tmp_path / "identity.json"),
|
|
}
|
|
|
|
|
|
def _set_env(monkeypatch, tmp_path) -> None:
|
|
for key, value in _base_env(tmp_path).items():
|
|
monkeypatch.setenv(key, value)
|
|
|
|
|
|
def _patch_create_application(monkeypatch) -> dict:
|
|
captured: dict = {}
|
|
|
|
def fake_create_application(*, config=None, **kwargs):
|
|
captured["config"] = config
|
|
app = _RecordingApplication()
|
|
captured["app"] = app
|
|
return app
|
|
|
|
monkeypatch.setattr(cli, "create_application", fake_create_application)
|
|
return captured
|
|
|
|
|
|
def test_existing_account_fast_path_skips_prompting(monkeypatch, tmp_path) -> None:
|
|
_set_env(monkeypatch, tmp_path)
|
|
LocalAccountStore(tmp_path / "account.json").create(
|
|
"operator", "correct horse battery staple"
|
|
)
|
|
|
|
def fail_input(prompt: str = "") -> str:
|
|
raise AssertionError("must not prompt when a local account already exists")
|
|
|
|
monkeypatch.setattr("builtins.input", fail_input)
|
|
captured = _patch_create_application(monkeypatch)
|
|
|
|
cli.main([])
|
|
|
|
assert captured["app"].ran is True
|
|
assert captured["config"].display_name == "operator"
|
|
|
|
|
|
def test_interactive_first_run_prompts_and_creates_account(
|
|
monkeypatch, tmp_path
|
|
) -> None:
|
|
_set_env(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
|
inputs = iter(["operator"])
|
|
monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs))
|
|
passwords = iter(["hunter2", "hunter2"])
|
|
monkeypatch.setattr("getpass.getpass", lambda prompt="": next(passwords))
|
|
captured = _patch_create_application(monkeypatch)
|
|
|
|
cli.main([])
|
|
|
|
assert captured["app"].ran is True
|
|
account = LocalAccountStore(tmp_path / "account.json").load()
|
|
assert account is not None
|
|
assert account.username == "operator"
|
|
|
|
|
|
def test_non_interactive_without_account_exits_with_clear_error(
|
|
monkeypatch, tmp_path, capsys
|
|
) -> None:
|
|
_set_env(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(sys.stdin, "isatty", lambda: False)
|
|
captured = _patch_create_application(monkeypatch)
|
|
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
cli.main([])
|
|
|
|
assert exc_info.value.code == 1
|
|
assert "setup" in capsys.readouterr().err
|
|
assert "app" not in captured
|
|
assert LocalAccountStore(tmp_path / "account.json").load() is None
|
|
|
|
|
|
def test_setup_subcommand_creates_account(monkeypatch, tmp_path) -> None:
|
|
_set_env(monkeypatch, tmp_path)
|
|
inputs = iter(["operator"])
|
|
monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs))
|
|
passwords = iter(["hunter2", "hunter2"])
|
|
monkeypatch.setattr("getpass.getpass", lambda prompt="": next(passwords))
|
|
captured = _patch_create_application(monkeypatch)
|
|
|
|
cli.main(["setup"])
|
|
|
|
assert "app" not in captured
|
|
account = LocalAccountStore(tmp_path / "account.json").load()
|
|
assert account is not None
|
|
assert account.username == "operator"
|
|
|
|
|
|
def test_setup_subcommand_refuses_overwrite_without_confirmation(
|
|
monkeypatch, tmp_path
|
|
) -> None:
|
|
_set_env(monkeypatch, tmp_path)
|
|
store = LocalAccountStore(tmp_path / "account.json")
|
|
original = store.create("operator", "original-password")
|
|
monkeypatch.setattr("builtins.input", lambda prompt="": "n")
|
|
|
|
cli.main(["setup"])
|
|
|
|
assert store.load() == original
|
|
|
|
|
|
def test_duplicate_instance_exits_with_clear_error(
|
|
monkeypatch, tmp_path, capsys
|
|
) -> None:
|
|
_set_env(monkeypatch, tmp_path)
|
|
LocalAccountStore(tmp_path / "account.json").create(
|
|
"operator", "correct horse battery staple"
|
|
)
|
|
lock_path = tmp_path / "identity_dir" / "host_agent.lock"
|
|
|
|
def raising_create_application(*, config=None, **kwargs):
|
|
raise InstanceAlreadyRunningError(lock_path)
|
|
|
|
monkeypatch.setattr(cli, "create_application", raising_create_application)
|
|
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
cli.main([])
|
|
|
|
assert exc_info.value.code == 1
|
|
err = capsys.readouterr().err
|
|
assert "another Host Agent instance" in err
|
|
assert str(lock_path) in err
|
|
|
|
|
|
def test_mcp_token_subcommand_prints_token(tmp_path, capsys, monkeypatch) -> None:
|
|
monkeypatch.setenv("HOST_AGENT_IDENTITY_PATH", str(tmp_path / "host_identity.json"))
|
|
monkeypatch.setenv(
|
|
"HOST_AGENT_LOCAL_ACCOUNT_PATH", str(tmp_path / "host_local_account.json")
|
|
)
|
|
# Also set control plane URL to satisfy config loading
|
|
monkeypatch.setenv("HOST_AGENT_CONTROL_PLANE_URL", "https://cloud.example")
|
|
from host_agent.cli import main
|
|
|
|
main(["mcp-token"])
|
|
out = capsys.readouterr().out.strip()
|
|
assert len(out) >= 40 # token is ~43 chars
|
|
# Subsequent invocation prints the same token (idempotent).
|
|
main(["mcp-token"])
|
|
out2 = capsys.readouterr().out.strip()
|
|
assert out == out2
|