128 lines
4.4 KiB
Python
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"
|