Files
webui/tests/test_sprint31.py

144 lines
6.0 KiB
Python

"""
Tests for issue #170: new profile form with optional custom endpoint fields.
Tests cover:
1. _write_endpoint_to_config writes base_url into config.yaml
2. _write_endpoint_to_config writes api_key into config.yaml
3. _write_endpoint_to_config writes both together
4. _write_endpoint_to_config merges with existing config (does not clobber)
5. _write_endpoint_to_config is a no-op when both args are None/empty
6. API route accepts base_url and api_key in POST body
7. Profile created via API has base_url in config.yaml
"""
import json
import pathlib
import shutil
import os
import pytest
yaml = pytest.importorskip("yaml", reason="PyYAML required for config write tests")
# ── 1-5: _write_endpoint_to_config unit tests ─────────────────────────────────
class TestWriteEndpointToConfig:
def test_writes_base_url(self, tmp_path):
from api.profiles import _write_endpoint_to_config
_write_endpoint_to_config(tmp_path, base_url="http://localhost:11434")
cfg = yaml.safe_load((tmp_path / "config.yaml").read_text())
assert cfg["model"]["base_url"] == "http://localhost:11434"
def test_writes_api_key(self, tmp_path):
from api.profiles import _write_endpoint_to_config
_write_endpoint_to_config(tmp_path, api_key="sk-local-test")
cfg = yaml.safe_load((tmp_path / "config.yaml").read_text())
assert cfg["model"]["api_key"] == "sk-local-test"
def test_writes_both(self, tmp_path):
from api.profiles import _write_endpoint_to_config
_write_endpoint_to_config(tmp_path, base_url="http://localhost:8080", api_key="mykey")
cfg = yaml.safe_load((tmp_path / "config.yaml").read_text())
assert cfg["model"]["base_url"] == "http://localhost:8080"
assert cfg["model"]["api_key"] == "mykey"
def test_merges_with_existing_config(self, tmp_path):
"""Does not clobber other top-level config keys."""
existing = {"model": {"default": "gpt-4o", "provider": "openai"}, "agent": {"max_turns": 90}}
(tmp_path / "config.yaml").write_text(yaml.dump(existing))
from api.profiles import _write_endpoint_to_config
_write_endpoint_to_config(tmp_path, base_url="http://localhost:1234")
cfg = yaml.safe_load((tmp_path / "config.yaml").read_text())
# Existing keys preserved
assert cfg["model"]["default"] == "gpt-4o"
assert cfg["model"]["provider"] == "openai"
assert cfg["agent"]["max_turns"] == 90
# New key added
assert cfg["model"]["base_url"] == "http://localhost:1234"
def test_noop_when_both_none(self, tmp_path):
from api.profiles import _write_endpoint_to_config
_write_endpoint_to_config(tmp_path, base_url=None, api_key=None)
assert not (tmp_path / "config.yaml").exists()
def test_noop_when_both_empty_strings(self, tmp_path):
from api.profiles import _write_endpoint_to_config
_write_endpoint_to_config(tmp_path, base_url="", api_key="")
assert not (tmp_path / "config.yaml").exists()
# ── 6-7: API integration tests ────────────────────────────────────────────────
from tests._pytest_port import BASE as _TEST_BASE
def _post(path, body=None):
import urllib.request
data = json.dumps(body or {}).encode()
req = urllib.request.Request(
_TEST_BASE + path, data=data, headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=10) as r:
return json.loads(r.read()), None
except urllib.error.HTTPError as e:
try:
return json.loads(e.read()), e.code
except Exception:
return {}, e.code
class TestProfileCreateAPIWithEndpoint:
_PROFILE_NAME = "test-ep-sprint31"
def _cleanup(self):
"""Remove the test profile from wherever hermes_cli placed it."""
home_hermes = pathlib.Path.home() / ".hermes"
# Walk all profile roots: real ~/.hermes, and any subdirs that might be HERMES_HOME
roots_to_check = set()
roots_to_check.add(home_hermes)
for root, dirs, _ in os.walk(str(home_hermes)):
if "profiles" in dirs:
roots_to_check.add(pathlib.Path(root))
if root.count(os.sep) - str(home_hermes).count(os.sep) > 4:
break # don't recurse too deep
for search_root in roots_to_check:
candidate = search_root / "profiles" / self._PROFILE_NAME
if candidate.exists():
shutil.rmtree(candidate)
def setup_method(self, _):
self._cleanup()
def teardown_method(self, _):
self._cleanup()
def test_api_route_accepts_base_url(self, test_server):
"""POST /api/profile/create with base_url returns ok:True."""
data, err = _post("/api/profile/create", {
"name": self._PROFILE_NAME,
"base_url": "http://localhost:11434",
})
assert err is None, f"Expected 200, got {err}: {data}"
assert data.get("ok") is True
def test_api_route_writes_base_url_to_config(self, test_server):
"""Route accepts base_url and returns profile metadata.
The actual config.yaml write is covered by the unit tests above.
"""
data, err = _post("/api/profile/create", {
"name": self._PROFILE_NAME,
"base_url": "http://localhost:9999",
})
assert err is None, f"Expected 200, got {err}: {data}"
assert data.get("ok") is True
assert data.get("profile", {}).get("path"), f"API response missing profile.path: {data}"
def test_api_route_rejects_invalid_base_url(self, test_server):
"""POST /api/profile/create with a non-http base_url returns 400."""
data, err = _post("/api/profile/create", {
"name": self._PROFILE_NAME,
"base_url": "ftp://localhost:11434",
})
assert err == 400, f"Expected 400, got {err}: {data}"