Initial commit: llamacppctl – llama.cpp Docker server control CLI
Steuert einen llama.cpp-Server als Docker-Container: --start/--check/--stop/
--change/--chat, INI-Konfiguration (builtin defaults -> [default] ->
[model.<profile>] -> CLI), SSRF-gehärtete Prompt-Eingabe (Datei/HTTPS-URL),
File-Locking für --start/--change und ein OpenAI-kompatibler HTTP-Layer.
Enthält u. a.:
- Env-Var-Expansion in hf_home (hf_home = ${HF_HOME})
- konfigurierbares Chat-Antwortbudget (max_tokens/chat_temperature,
CLI: --max-tokens/--chat-temp); temperature defer an Server-Default
- DNS-Pinning gegen DNS-Rebinding bei URL-Quellen
- dry-run als nebenwirkungsfreie Vorschau (kein Lock/Removal/Modell-Check)
- 98 Tests (pytest)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
3158d16f9b
32 changed files with 3912 additions and 0 deletions
145
tests/test_cli.py
Normal file
145
tests/test_cli.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
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"])
|
||||
|
||||
|
||||
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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue