test: close coverage gaps + opt-in integration smoke

Unit-Tests (kein Docker nötig), 130 Tests gesamt:
- config: ${ENV}- und ~-Expansion in hf_home
- prompt_io: _pin_dns (DNS-Rebinding wird abgewiesen, andere Hosts unberührt,
  Resolver wird wiederhergestellt)
- actions: do_check (running/healthy, missing), do_start non-dry-run
  (Happy-Path + Readiness-Fehler mit --logs), _container_lock Force-Bypass
- cli: --max-tokens <= 0 abgelehnt, --expose/--no-expose

scripts/smoke.sh: opt-in End-to-End-Test gegen echten Docker + llama.cpp-Server
(--api-key-Round-Trip inkl. 401/200, start/check/chat/stream/stop, eigener
Container/Port, Cleanup-Trap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-07-06 17:08:10 +02:00
commit f83f36fdfb
6 changed files with 268 additions and 4 deletions

View file

@ -4,8 +4,12 @@ from types import SimpleNamespace
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from llamacppctl import actions # noqa: E402
import pytest # noqa: E402
from llamacppctl import actions, http_ops # noqa: E402
from llamacppctl.docker_ops import ContainerInfo # noqa: E402
from llamacppctl.http_ops import HttpError # noqa: E402
from llamacppctl.lock_ops import FileLock, LockError # noqa: E402
from llamacppctl.schema import ChatReply, CheckResult # noqa: E402
from tests.test_docker_ops import make_cfg # noqa: E402
@ -125,3 +129,87 @@ def test_do_stop_passes_force(tmp_path, monkeypatch):
monkeypatch.setattr(actions, "stop_container", fake_stop)
actions.do_stop(cfg, SimpleNamespace(force=True))
assert seen["force"] is True
# --- do_check ------------------------------------------------------------
def test_do_check_running_and_healthy(tmp_path, monkeypatch):
cfg = _cfg(tmp_path)
monkeypatch.setattr(
actions, "inspect_container", lambda n: ContainerInfo(n, "running", "healthy")
)
monkeypatch.setattr(http_ops, "check_health", lambda c: True)
monkeypatch.setattr(http_ops, "check_models", lambda c: True)
monkeypatch.setattr(actions, "chat_completion_text", lambda c, p: ChatReply("ok", "stop"))
result = actions.do_check(cfg, SimpleNamespace(user_prompt="ping", max_tokens=8), SimpleNamespace())
assert result.container_running is True
assert result.http_ok is True
assert result.chat_ok is True
assert actions.check_exit_code(result) == actions.EXIT_OK
def test_do_check_missing_container(tmp_path, monkeypatch):
cfg = _cfg(tmp_path)
monkeypatch.setattr(
actions, "inspect_container", lambda n: ContainerInfo(n, "missing", "none")
)
result = actions.do_check(cfg, SimpleNamespace(user_prompt=None, max_tokens=8), SimpleNamespace())
assert result.container_exists is False
assert result.container_running is False
assert result.http_ok is False
assert actions.check_exit_code(result) == actions.EXIT_HTTP_NOT_READY
# --- do_start (non-dry-run) ----------------------------------------------
def _wire_start(monkeypatch, ready: bool):
monkeypatch.setattr(actions, "validate_model_path", lambda c: None)
monkeypatch.setattr(actions, "start_llama_container", lambda c: "abc123def456")
monkeypatch.setattr(actions, "wait_until_ready", lambda c, p: ready)
monkeypatch.setattr(actions, "container_logs", lambda n, t=100: "log tail")
def test_do_start_happy_path(tmp_path, monkeypatch, capsys):
cfg = _cfg(tmp_path)
_wire_start(monkeypatch, ready=True)
monkeypatch.setattr(actions, "chat_completion_text", lambda c, p: ChatReply("Servus", "stop"))
prompt = SimpleNamespace(user_prompt="hi", max_tokens=2048)
rc = actions.do_start(cfg, prompt, SimpleNamespace(dry_run=False, force=False, logs=False))
out = capsys.readouterr().out
assert rc == actions.EXIT_OK
assert "Model ready." in out
assert "Servus" in out
def test_do_start_readiness_failure(tmp_path, monkeypatch, capsys):
cfg = _cfg(tmp_path)
_wire_start(monkeypatch, ready=False)
prompt = SimpleNamespace(user_prompt=None, max_tokens=2048)
rc = actions.do_start(cfg, prompt, SimpleNamespace(dry_run=False, force=False, logs=True, log_lines=50))
err = capsys.readouterr().err
assert rc == actions.EXIT_HTTP_NOT_READY
assert "did not become ready" in err
assert "log tail" in err # --logs printed the tail
# --- _container_lock force bypass ----------------------------------------
def test_container_lock_busy_raises_without_force(tmp_path):
cfg = _cfg(tmp_path)
with FileLock(cfg.lock_file): # hold the lock
with pytest.raises(LockError):
with actions._container_lock(cfg, SimpleNamespace(force=False)):
pass
def test_container_lock_force_bypasses_busy(tmp_path, capsys):
cfg = _cfg(tmp_path)
entered = False
with FileLock(cfg.lock_file): # hold the lock
with actions._container_lock(cfg, SimpleNamespace(force=True)):
entered = True
assert entered is True
assert "busy lock" in capsys.readouterr().err