2026-07-06 16:36:27 +02:00
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
|
|
|
|
|
|
from llamacppctl.cli import build_parser, validate_args # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse(argv):
|
|
|
|
|
parser = build_parser()
|
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
|
validate_args(args, parser)
|
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_error(argv):
|
|
|
|
|
parser = build_parser()
|
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
|
validate_args(args, parser)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_requires_one_action():
|
|
|
|
|
parse_error([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_actions_are_mutually_exclusive():
|
|
|
|
|
parse_error(["--start", "--stop"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_start_is_valid_minimal():
|
|
|
|
|
args = parse(["--start"])
|
|
|
|
|
assert args.start is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_chat_requires_prompt_source():
|
|
|
|
|
parse_error(["--chat"])
|
|
|
|
|
|
|
|
|
|
|
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>
2026-07-06 17:08:10 +02:00
|
|
|
def test_max_tokens_nonpositive_rejected():
|
|
|
|
|
parse_error(["--start", "--max-tokens", "0"])
|
|
|
|
|
parse_error(["--start", "--max-tokens", "-5"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_max_tokens_positive_ok():
|
|
|
|
|
args = parse(["--start", "--max-tokens", "4096"])
|
|
|
|
|
assert args.max_tokens == 4096
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_expose_and_no_expose_flags():
|
|
|
|
|
assert parse(["--start", "--expose"]).expose is True
|
|
|
|
|
assert parse(["--start", "--no-expose"]).expose is False
|
|
|
|
|
assert parse(["--start"]).expose is None # unset => config decides
|
|
|
|
|
|
|
|
|
|
|
2026-07-06 16:36:27 +02:00
|
|
|
def test_chat_with_prompt_is_valid():
|
|
|
|
|
args = parse(["--chat", "-p", "hello"])
|
|
|
|
|
assert args.chat is True
|
|
|
|
|
assert args.prompt == "hello"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_stop_rejects_prompt_sources():
|
|
|
|
|
parse_error(["--stop", "-p", "hello"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_stop_rejects_system_sources():
|
|
|
|
|
parse_error(["--stop", "-s", "system text"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_check_with_system_but_no_prompt_rejected():
|
|
|
|
|
parse_error(["--check", "-s", "system text"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_check_with_system_and_prompt_ok():
|
|
|
|
|
args = parse(["--check", "-s", "system text", "-p", "hello"])
|
|
|
|
|
assert args.check is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mutually_exclusive_system_sources():
|
|
|
|
|
parse_error(["--start", "-s", "a", "--system-file", "b.txt"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mutually_exclusive_prompt_sources():
|
|
|
|
|
parse_error(["--start", "-p", "a", "--prompt-url", "https://example.com/p"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_max_input_bytes_must_be_positive():
|
|
|
|
|
parse_error(["--start", "--max-input-bytes", "0"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_negative_max_input_bytes_rejected():
|
|
|
|
|
parse_error(["--start", "--max-input-bytes", "-5"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_connect_timeout_must_be_positive():
|
|
|
|
|
parse_error(["--start", "--connect-timeout", "0"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_read_timeout_must_be_positive():
|
|
|
|
|
parse_error(["--start", "--read-timeout", "-1"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_allow_private_url_conflicts_with_allowlist():
|
|
|
|
|
parse_error(["--start", "--allow-private-url", "--url-allow-host", "example.com"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_host_port_out_of_range_rejected():
|
|
|
|
|
parse_error(["--start", "--host-port", "70000"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_host_port_zero_rejected():
|
|
|
|
|
parse_error(["--start", "--host-port", "0"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_container_port_out_of_range_rejected():
|
|
|
|
|
parse_error(["--start", "--container-port", "-1"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_container_name_empty_string_rejected():
|
|
|
|
|
parse_error(["--start", "--container-name", " "])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_container_name_valid():
|
|
|
|
|
args = parse(["--start", "--container-name", "my_llm"])
|
|
|
|
|
assert args.container_name == "my_llm"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_boolean_flag_pairs_default_none():
|
|
|
|
|
args = parse(["--start"])
|
|
|
|
|
assert args.jinja is None
|
|
|
|
|
assert args.fa is None
|
|
|
|
|
assert args.kv_unified is None
|
|
|
|
|
assert args.cont_batching is None
|
|
|
|
|
assert args.no_context_shift is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_boolean_flag_pairs_explicit_true_false():
|
|
|
|
|
args = parse(["--start", "--jinja", "--no-fa"])
|
|
|
|
|
assert args.jinja is True
|
|
|
|
|
assert args.fa is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_dry_run_flag():
|
|
|
|
|
args = parse(["--start", "--dry-run"])
|
|
|
|
|
assert args.dry_run is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_print_effective_config_flag():
|
|
|
|
|
args = parse(["--start", "--print-effective-config"])
|
|
|
|
|
assert args.print_effective_config is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reasoning_choice_validated():
|
|
|
|
|
parse_error(["--start", "--reasoning", "maybe"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reasoning_choice_valid():
|
|
|
|
|
args = parse(["--start", "--reasoning", "off"])
|
|
|
|
|
assert args.reasoning == "off"
|