Files

203 lines
12 KiB
Python

import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("prepare", ROOT / "scripts/prepare-workstation.py")
prepare = importlib.util.module_from_spec(spec)
spec.loader.exec_module(prepare)
class PreparationTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.data = b"test model weights"
self.item = {"name": "weights.gguf", "sha256": hashlib.sha256(self.data).hexdigest(), "size": len(self.data)}
self.group = {"repo": "owner/model", "revision": "a" * 40, "directory": "model/revision", "files": [self.item]}
def test_download_uses_fixed_revision_and_publishes_only_verified_file(self):
def download(args, **kwargs):
self.assertIn("/resolve/" + "a" * 40 + "/weights.gguf", args[-3])
Path(args[-1]).write_bytes(self.data)
with patch.object(prepare.subprocess, "run", side_effect=download):
paths = prepare.ensure_model(self.group, self.root, True)
self.assertEqual(paths[0].read_bytes(), self.data)
self.assertFalse(paths[0].with_name("weights.gguf.part").exists())
def test_corrupt_download_cannot_become_live_model(self):
with patch.object(prepare.subprocess, "run", side_effect=lambda args, **_: Path(args[-1]).write_bytes(b"broken")):
with self.assertRaisesRegex(ValueError, "nicht aktiviert"):
prepare.ensure_model(self.group, self.root, True)
self.assertFalse((self.root / "model/revision/weights.gguf").exists())
def test_existing_wrong_file_is_preserved_without_download(self):
path = self.root / "model/revision/weights.gguf"
path.parent.mkdir(parents=True)
path.write_bytes(b"user file")
with patch.object(prepare.subprocess, "run") as download, self.assertRaises(ValueError):
prepare.ensure_model(self.group, self.root, True)
download.assert_not_called()
self.assertEqual(path.read_bytes(), b"user file")
def test_config_merge_preserves_unrelated_settings_and_external_provider(self):
config = {"terminal": {"backend": "docker"}, "plugins": {"enabled": ["private"]},
"custom_providers": [{"name": "Other", "base_url": "https://other.invalid/v1", "model": "other"}]}
updated = prepare.configure_hermes(config, True)
self.assertEqual(updated["terminal"]["backend"], "docker")
self.assertEqual(updated["plugins"]["enabled"], ["private"])
self.assertEqual(updated["custom_providers"][0]["model"], "other")
self.assertEqual(updated["custom_providers"][1]["extra_body"]["max_tokens"], 2048)
self.assertEqual(updated["auxiliary"]["compression"]["base_url"], "http://127.0.0.1:8081/v1")
def test_context_and_compression_follow_selected_model(self):
for name, window, trigger in (("routine", 65536, 24000), ("fallback", 65536, 24000), ("large", 65536, 24000)):
with self.subTest(name=name):
config = {"model": {"context_length": 256000}, "compression": {"threshold_tokens": 24000}}
prepare.configure_hermes(config, context=window)
self.assertEqual(config["model"]["context_length"], window)
self.assertEqual(config["compression"]["threshold_tokens"], trigger)
text = prepare.profile_config(name, [self.root / "weights.gguf"])
self.assertIn(f"--ctx-size {window}", text)
before = json.dumps(config, sort_keys=True)
prepare.configure_hermes(config, context=window)
self.assertEqual(json.dumps(config, sort_keys=True), before)
def test_configuration_refuses_below_hermes_context_floor(self):
config = {"model": {"context_length": 65536}}
with self.assertRaisesRegex(ValueError, "at least 64000"):
prepare.configure_hermes(config, context=16384)
self.assertEqual(config, {"model": {"context_length": 65536}})
def test_existing_profiles_preserved_and_explicit_selection_backed_up(self):
path = self.root / "llama.env"
path.write_text("custom tuning\n")
prepare.write_config(path, "generated\n")
self.assertEqual(path.read_text(), "custom tuning\n")
prepare.write_config(path, "generated\n", replace=True)
prepare.write_config(path, "generated\n", replace=True)
self.assertEqual(len(list(self.root.glob("llama.env.before-setup-*"))), 1)
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
def test_profiles_quote_other_user_paths_and_do_not_use_remote_hf_start(self):
text = prepare.profile_config("routine", [self.root / "user space/model.gguf", self.root / "projector.gguf"])
result = subprocess.run(["bash", "-c", text + '\nprintf "%s|%s|%s" "$LLAMA_MODEL" "$LLAMA_HF_REPO" "${LLAMA_ARGS[*]}"'], text=True, capture_output=True, check=True)
self.assertTrue(result.stdout.startswith(str(self.root / "user space/model.gguf") + "||"))
self.assertIn("--n-gpu-layers 99 --fit off", result.stdout)
def test_prepare_whole_new_workstation_offline_without_home_override(self):
repository = self.root / "repository"
(repository / "config").mkdir(parents=True)
shutil.copy2(ROOT / "config/config.env.example", repository / "config")
(repository / "config/models.lock.json").write_text(json.dumps({"models": {"routine": self.group}}))
path = self.root / "models/model/revision/weights.gguf"
path.parent.mkdir(parents=True)
path.write_bytes(self.data)
config = self.root / "config/hermes-stack-maintenance"
hermes = self.root / "hermes/config.yaml"
args = ["prepare", "--models", "routine", "--model-dir", str(self.root / "models"),
"--config-dir", str(config), "--hermes-config", str(hermes),
"--configure-hermes", "--select", "routine"]
with patch.object(prepare, "ROOT", repository), patch("sys.argv", args), \
patch.dict(os.environ, {"XDG_RUNTIME_DIR": str(self.root)}), \
patch.object(prepare.subprocess, "check_output", return_value="inactive\n"):
prepare.main()
self.assertEqual((config / "llama.env").read_text(), (config / "llama-routine.env").read_text())
self.assertIn("LLAMA_MAX_REQUEST_SECONDS=600", (config / "config.env").read_text())
self.assertIn("run_budget_seconds: 1800", hermes.read_text())
def test_local_cron_unpin_does_not_touch_other_provider_jobs(self):
path = self.root / "jobs.json"
path.write_text(json.dumps({"jobs": [
{"id": "local", "model": "qwen38-27b-local", "provider": "custom"},
{"id": "cloud", "model": "qwen38-27b-local", "provider": "other"},
{"id": "remote", "model": "qwen38-27b-local", "provider": "custom", "base_url": "https://other.invalid/v1"}]}))
with patch.object(prepare.subprocess, "run") as edit:
prepare.unpin_local_jobs(path)
edit.assert_called_once_with(["hermes", "cron", "edit", "local", "--unpin"], check=True)
class ModelSwitchTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.bin = self.root / "bin"
self.bin.mkdir()
self.config = self.root / "config/hermes-stack-maintenance"
self.config.mkdir(parents=True)
(self.config / "llama.env").write_text("LLAMA_MODEL=old.gguf\n")
(self.config / "llama-routine.env").write_text("LLAMA_MODEL=new.gguf\n")
shutil.copy2(ROOT / "libexec/hermes-stack-model", self.bin)
for name, body in {"hermes-stack-llama-run": "exit 0",
"systemctl": 'echo "$*" >>"$TEST_CALLS"; if [[ "$*" == *"restart llama-qwen"* && "${TEST_FAIL:-0}" == 1 ]]; then exit 1; fi',
"curl": "echo '{\"status\":\"ok\"}'"}.items():
p = self.bin / name
p.write_text("#!/usr/bin/env bash\n" + body + "\n")
p.chmod(0o755)
self.env = dict(os.environ, PATH=str(self.bin) + ":" + os.environ["PATH"],
HERMES_HOME=str(self.root / "hermes"), XDG_CONFIG_HOME=str(self.config.parent), XDG_RUNTIME_DIR=str(self.root), TEST_CALLS=str(self.root / "calls"))
def test_switch_and_failure_keep_hermes_window_paired_with_model(self):
import yaml
directory = self.root / "hermes"
directory.mkdir()
path = directory / "config.yaml"
original = "model:\n context_length: 65536\ncompression:\n threshold_tokens: 24000\nterminal:\n backend: docker\n"
(self.config / "llama-routine.env").write_text("LLAMA_MODEL=new.gguf\nLLAMA_CONTEXT_TOKENS=64000\n")
for fail in (False, True):
path.write_text(original)
(self.config / "llama.env").write_text("LLAMA_MODEL=old.gguf\n")
result = subprocess.run(["bash", str(self.bin / "hermes-stack-model"), "routine"],
env=dict(self.env, TEST_FAIL=str(int(fail))), capture_output=True, text=True)
if fail:
self.assertNotEqual(result.returncode, 0)
self.assertEqual(path.read_text(), original)
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=old.gguf\n")
else:
self.assertEqual(result.returncode, 0, result.stderr)
config = yaml.safe_load(path.read_text())
self.assertEqual(config["model"]["context_length"], 64000)
self.assertEqual(config["compression"]["threshold_tokens"], 24000)
self.assertEqual(config["terminal"]["backend"], "docker")
def test_hermes_rejects_too_small_context_and_restores_pair(self):
directory = self.root / "hermes"
directory.mkdir()
path = directory / "config.yaml"
original = "model:\n context_length: 65536\n"
path.write_text(original)
(self.config / "llama-routine.env").write_text("LLAMA_MODEL=new.gguf\nLLAMA_CONTEXT_TOKENS=16384\n")
result = subprocess.run(["bash", str(self.bin / "hermes-stack-model"), "routine"],
env=self.env, capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn("at least 64000", result.stderr)
self.assertEqual(path.read_text(), original)
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=old.gguf\n")
def test_successful_switch_through_path_entrypoint(self):
(self.root / "entrypoint").symlink_to(self.bin / "hermes-stack-model")
result = subprocess.run(["bash", str(self.root / "entrypoint"), "routine"], env=self.env, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=new.gguf\n")
self.assertIn("start hermes-gateway.service", (self.root / "calls").read_text())
def test_failed_switch_restores_configuration_and_still_attempts_gateway_start(self):
result = subprocess.run(["bash", str(self.bin / "hermes-stack-model"), "routine"],
env=dict(self.env, TEST_FAIL="1"), capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=old.gguf\n")
self.assertIn("start hermes-gateway.service", (self.root / "calls").read_text())
if __name__ == "__main__":
unittest.main()