honcho/honcho-cli/tests/test_local.py

128 lines
4.4 KiB
Python

"""Local-stack contracts: profile files, env merge, image pin, port remap."""
from __future__ import annotations
import json
import os
import subprocess
import pytest
from honcho_cli.local.docker import (
DockerError,
allocate_host_ports,
pin_image,
seed_config_toml,
)
from honcho_cli.local.env import managed_env, read_env_value, render_stack, upsert_env
from honcho_cli.local.profile import LocalProfile, load_profile, save_profile
@pytest.fixture
def cfg_dir(tmp_path, monkeypatch):
monkeypatch.setattr("honcho_cli.config.CONFIG_DIR", tmp_path)
monkeypatch.setattr("honcho_cli.config.CONFIG_FILE", tmp_path / "config.json")
for k in [k for k in os.environ if k.startswith("HONCHO_")]:
monkeypatch.delenv(k)
return tmp_path
def test_profile_roundtrip_has_no_secrets(cfg_dir):
profile = LocalProfile(
name="local",
api_port=8001,
image="ghcr.io/plastic-labs/honcho@sha256:abc",
)
save_profile(profile)
loaded = load_profile("local")
assert loaded.api_port == 8001
assert loaded.image.endswith("@sha256:abc")
on_disk = json.loads(profile.profile_file().read_text())
assert "LLM" not in json.dumps(on_disk)
assert set(on_disk) == {"apiPort", "dbPort", "redisPort", "image"}
def test_upsert_preserves_extra_env_keys(tmp_path):
path = tmp_path / ".env"
path.write_text("CUSTOM_FLAG=keep-me\n# user comment\n")
upsert_env(path, managed_env(LocalProfile(name="local")))
text = path.read_text()
assert "CUSTOM_FLAG=keep-me" in text
assert "user comment" in text
assert text.count("Generated by honcho start") == 1
def test_upsert_writes_non_managed_and_preserves_later(tmp_path):
path = tmp_path / ".env"
first = managed_env(LocalProfile(name="local"))
first["DERIVER_MODEL_CONFIG__MODEL"] = "gpt-test"
upsert_env(path, first)
upsert_env(path, managed_env(LocalProfile(name="local")))
later = path.read_text()
assert "DERIVER_MODEL_CONFIG__MODEL=gpt-test" in later
def test_render_stack_uses_published_image(cfg_dir):
profile = LocalProfile(name="local")
render_stack(profile)
compose = profile.compose_file().read_text()
assert "ghcr.io/plastic-labs/honcho" in compose
assert "build:" not in compose
assert compose.count("./config.toml:/app/config.toml:ro") == 2
assert read_env_value(profile.env_file(), "AUTH_USE_AUTH") == "false"
assert oct(profile.env_file().stat().st_mode)[-3:] == "600"
def test_pin_latest_to_matching_digest(monkeypatch):
pulls: list[str] = []
def fake_run(args, *, check=False):
if args[:1] == ["pull"]:
pulls.append(args[1])
return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
if args[:2] == ["image", "inspect"]:
body = json.dumps(
[
"ghcr.io/plastic-labs/honcho@sha256:deadbeef",
"ghcr.io/other/honcho@sha256:nope",
]
)
return subprocess.CompletedProcess(args, 0, stdout=body, stderr="")
raise AssertionError(args)
monkeypatch.setattr("honcho_cli.local.docker._run_docker", fake_run)
assert pin_image("ghcr.io/plastic-labs/honcho:latest") == (
"ghcr.io/plastic-labs/honcho@sha256:deadbeef"
)
assert pulls == ["ghcr.io/plastic-labs/honcho:latest"]
def test_seed_config_toml_writes_once(cfg_dir, monkeypatch):
profile = LocalProfile(
name="local", image="ghcr.io/plastic-labs/honcho@sha256:abc"
)
monkeypatch.setattr(
"honcho_cli.local.docker._copy_from_image",
lambda image, paths: "[deriver]\nWORKERS = 2\n",
)
assert seed_config_toml(profile) is True
profile.config_file().write_text(
profile.config_file().read_text() + "# user edit\n"
)
assert seed_config_toml(profile) is False
assert "# user edit" in profile.config_file().read_text()
def test_busy_port_remaps_unless_pinned(monkeypatch):
monkeypatch.setattr(
"honcho_cli.local.docker.port_available",
lambda port, host="127.0.0.1": port != 6379,
)
profile, remapped = allocate_host_ports(LocalProfile(name="local"))
assert profile.redis_port == 6380
assert remapped["redis"] == (6379, 6380)
with pytest.raises(DockerError) as exc:
allocate_host_ports(LocalProfile(name="local"), pinned=frozenset({"redis"}))
assert exc.value.code == "PORT_IN_USE"
assert exc.value.details["flag"] == "--redis-port"