push
Some checks failed
CI / test (push) Failing after 1s

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-04 03:43:41 +02:00
parent 26c043863e
commit 17fd96680b
15 changed files with 956 additions and 2 deletions

86
tests/test_cli.py Normal file
View File

@@ -0,0 +1,86 @@
from typer.testing import CliRunner
from tai.cli import app
from tai.ssh_client import SSHCommandResult
def test_run_command_prints_scaffold_summary() -> None:
runner = CliRunner()
result = runner.invoke(
app,
[
"apache failed",
"--host",
"web01",
"--port",
"5566",
"--no-probe",
"--path",
"/etc/apache2",
"--jump-host",
"bastion01",
"--ignore-ssh-config",
],
)
assert result.exit_code == 0
assert "tai scaffold ready" in result.stdout
assert "host=web01" in result.stdout
assert "port=5566" in result.stdout
def test_probe_success_prints_remote_output_by_default(monkeypatch) -> None: # type: ignore[no-untyped-def]
async def fake_probe(self) -> SSHCommandResult: # type: ignore[no-untyped-def]
return SSHCommandResult(
command="uname -a",
exit_code=0,
stdout="Linux ssh 6.12.0",
stderr="",
)
monkeypatch.setattr("tai.cli.SSHClient.probe", fake_probe)
runner = CliRunner()
result = runner.invoke(
app,
[
"apache failed",
"--host",
"ssh.archflux.net",
"--port",
"5566",
"--probe",
],
)
assert result.exit_code == 0
assert "Probe succeeded" in result.stdout
assert "Linux ssh 6.12.0" in result.stdout
def test_probe_failure_returns_non_zero(monkeypatch) -> None: # type: ignore[no-untyped-def]
async def fake_probe(self) -> SSHCommandResult: # type: ignore[no-untyped-def]
return SSHCommandResult(
command="uname -a",
exit_code=255,
stdout="",
stderr="Permission denied (publickey,password).",
)
monkeypatch.setattr("tai.cli.SSHClient.probe", fake_probe)
runner = CliRunner()
result = runner.invoke(
app,
[
"apache failed",
"--host",
"ssh.archflux.net",
"--port",
"5566",
"--probe",
],
)
assert result.exit_code == 1
assert "Probe failed" in result.stdout

View File

@@ -0,0 +1,65 @@
from pathlib import Path
import pytest
from tai.input_parser import InputValidationError, build_request
def test_build_request_normalizes_values() -> None:
req = build_request(
issue=" apache fails to start ",
host=" web01 ",
port=5566,
target_paths=["/etc/apache2", "~/logs"],
identity_file="~/.ssh/id_ed25519",
jump_host=" bastion01 ",
ignore_ssh_config=True,
)
assert req.issue == "apache fails to start"
assert req.host == "web01"
assert req.port == 5566
assert req.target_paths[0] == Path("/etc/apache2")
assert req.target_paths[1] == Path("~/logs").expanduser()
assert req.identity_file == Path("~/.ssh/id_ed25519").expanduser()
assert req.jump_host == "bastion01"
assert req.ignore_ssh_config is True
def test_build_request_rejects_empty_issue() -> None:
with pytest.raises(InputValidationError):
build_request(
issue=" ",
host="web01",
port=22,
target_paths=[],
identity_file=None,
jump_host=None,
ignore_ssh_config=False,
)
def test_build_request_rejects_empty_host() -> None:
with pytest.raises(InputValidationError):
build_request(
issue="apache down",
host=" ",
port=22,
target_paths=[],
identity_file=None,
jump_host=None,
ignore_ssh_config=False,
)
def test_build_request_rejects_invalid_port() -> None:
with pytest.raises(InputValidationError):
build_request(
issue="apache down",
host="web01",
port=70000,
target_paths=[],
identity_file=None,
jump_host=None,
ignore_ssh_config=False,
)

93
tests/test_ssh_client.py Normal file
View File

@@ -0,0 +1,93 @@
from pathlib import Path
import pytest
from tai.ssh_client import SSHClient, SSHCommandRejectedError, SSHConnectionConfig
def _client(**kwargs: object) -> SSHClient:
host = str(kwargs.get("host", "root@ssh.archflux.net"))
port_value = kwargs.get("port", 22)
if not isinstance(port_value, int):
raise TypeError("port must be an int")
port = port_value
identity_file = kwargs.get("identity_file")
jump_host = kwargs.get("jump_host")
ignore_ssh_config = bool(kwargs.get("ignore_ssh_config", False))
if identity_file is not None and not isinstance(identity_file, Path):
raise TypeError("identity_file must be a Path or None")
if jump_host is not None and not isinstance(jump_host, str):
raise TypeError("jump_host must be a string or None")
return SSHClient(
SSHConnectionConfig(
host=host,
port=port,
identity_file=identity_file,
jump_host=jump_host,
ignore_ssh_config=ignore_ssh_config,
)
)
def test_summary_includes_expected_defaults() -> None:
client = _client()
text = client.summary()
assert "host=root@ssh.archflux.net" in text
assert "port=22" in text
assert "key=auto" in text
assert "jump=none" in text
assert "mode=use ssh config" in text
def test_build_ssh_argv_respects_flags() -> None:
client = _client(
identity_file=Path("/root/.ssh/id_ed25519"),
jump_host="bastion.archflux.net",
ignore_ssh_config=True,
)
argv = client.build_ssh_argv("uname -a")
assert argv[0] == "ssh"
assert "-p" in argv
assert "22" in argv
assert "-F" in argv
assert "/dev/null" in argv
assert "-i" in argv
assert "/root/.ssh/id_ed25519" in argv
assert "-J" in argv
assert "bastion.archflux.net" in argv
assert argv[-2] == "root@ssh.archflux.net"
assert argv[-1] == "uname -a"
def test_rejects_destructive_or_shell_operator_commands() -> None:
client = _client()
for command in ["rm -rf /tmp/x", "cat /etc/hosts | grep localhost", "uname -a; id"]:
with pytest.raises(SSHCommandRejectedError):
client.validate_read_only_command(command)
def test_allows_expected_read_only_commands() -> None:
client = _client()
for command in [
"uname -a",
"journalctl -n 100",
"systemctl status apache2",
"cat /etc/hosts",
"ss -lntp",
]:
client.validate_read_only_command(command)
def test_rejects_non_read_only_systemctl_subcommand() -> None:
client = _client()
with pytest.raises(SSHCommandRejectedError):
client.validate_read_only_command("systemctl restart apache2")