245 lines
8.5 KiB
Python
245 lines
8.5 KiB
Python
"""Onboarding MVP tests — first-run wizard and provider config persistence.
|
|
|
|
Tests that call /api/onboarding/setup require PyYAML in the test server's
|
|
Python environment (the agent venv). They are skipped when hermes-agent is
|
|
not installed, since the server falls back to system Python which typically
|
|
lacks pyyaml.
|
|
"""
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
import pytest
|
|
|
|
from tests._pytest_port import BASE
|
|
|
|
# Check if pyyaml is available — onboarding setup tests need it on the server
|
|
try:
|
|
import yaml as _yaml
|
|
_HAS_YAML = True
|
|
except ImportError:
|
|
_HAS_YAML = False
|
|
_needs_yaml = pytest.mark.skipif(not _HAS_YAML, reason="PyYAML not installed — onboarding setup tests require it")
|
|
|
|
|
|
def get(path):
|
|
with urllib.request.urlopen(BASE + path, timeout=10) as r:
|
|
return json.loads(r.read()), r.status
|
|
|
|
|
|
def post(path, body=None):
|
|
req = urllib.request.Request(
|
|
BASE + path,
|
|
data=json.dumps(body or {}).encode(),
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10) as r:
|
|
return json.loads(r.read()), r.status
|
|
except urllib.error.HTTPError as e:
|
|
return json.loads(e.read()), e.code
|
|
|
|
|
|
def _server_hermes_home() -> pathlib.Path:
|
|
"""Get the hermes home path the test server is actually using.
|
|
|
|
Using the server's own /api/onboarding/status response is more robust than
|
|
reading TEST_STATE_DIR from conftest, which can get the wrong path when
|
|
conftest is imported multiple times under different HERMES_HOME environments
|
|
(api.config resets HERMES_HOME at module import time via init_profile_state).
|
|
"""
|
|
data, _ = get("/api/onboarding/status")
|
|
env_path = data.get("system", {}).get("env_path", "")
|
|
if env_path:
|
|
return pathlib.Path(env_path).parent
|
|
# Fallback
|
|
hermes_home = pathlib.Path.home() / ".hermes"
|
|
return hermes_home / "webui-mvp-test"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_hermes_config_files():
|
|
hermes_home = _server_hermes_home()
|
|
for rel in ("config.yaml", ".env"):
|
|
(hermes_home / rel).unlink(missing_ok=True)
|
|
yield
|
|
for rel in ("config.yaml", ".env"):
|
|
(hermes_home / rel).unlink(missing_ok=True)
|
|
|
|
|
|
|
|
def test_onboarding_status_defaults_incomplete():
|
|
data, status = get("/api/onboarding/status")
|
|
assert status == 200
|
|
assert data["completed"] is False
|
|
assert data["settings"]["password_enabled"] is False
|
|
assert data["system"]["provider_configured"] is False
|
|
assert data["system"]["chat_ready"] is False
|
|
assert data["system"]["setup_state"] in {"needs_provider", "agent_unavailable"}
|
|
assert "provider_note" in data["system"]
|
|
assert isinstance(data["workspaces"]["items"], list)
|
|
assert data["setup"]["providers"]
|
|
|
|
|
|
@_needs_yaml
|
|
def test_onboarding_setup_openrouter_writes_real_config_and_env():
|
|
data, status = post(
|
|
"/api/onboarding/setup",
|
|
{
|
|
"provider": "openrouter",
|
|
"model": "anthropic/claude-sonnet-4.6",
|
|
"api_key": "sk-or-test",
|
|
},
|
|
)
|
|
assert status == 200
|
|
assert data["system"]["provider_configured"] is True
|
|
assert data["system"]["provider_ready"] is True
|
|
if data["system"]["imports_ok"] and data["system"]["hermes_found"]:
|
|
assert data["system"]["chat_ready"] is True
|
|
assert data["system"]["setup_state"] == "ready"
|
|
else:
|
|
assert data["system"]["chat_ready"] is False
|
|
assert data["system"]["setup_state"] == "agent_unavailable"
|
|
|
|
cfg_text = (_server_hermes_home() / "config.yaml").read_text(encoding="utf-8")
|
|
env_text = (_server_hermes_home() / ".env").read_text(encoding="utf-8")
|
|
assert "provider: openrouter" in cfg_text
|
|
assert "default: anthropic/claude-sonnet-4.6" in cfg_text
|
|
assert "OPENROUTER_API_KEY=sk-or-test" in env_text
|
|
|
|
|
|
@_needs_yaml
|
|
def test_onboarding_setup_custom_endpoint_writes_runtime_files():
|
|
data, status = post(
|
|
"/api/onboarding/setup",
|
|
{
|
|
"provider": "custom",
|
|
"model": "google/gemma-3-27b-it",
|
|
"base_url": "http://localhost:4000/v1",
|
|
"api_key": "sk-custom-test",
|
|
},
|
|
)
|
|
assert status == 200
|
|
assert data["system"]["provider_configured"] is True
|
|
assert data["system"]["provider_ready"] is True
|
|
if data["system"]["imports_ok"] and data["system"]["hermes_found"]:
|
|
assert data["system"]["chat_ready"] is True
|
|
assert data["system"]["setup_state"] == "ready"
|
|
else:
|
|
assert data["system"]["chat_ready"] is False
|
|
assert data["system"]["setup_state"] == "agent_unavailable"
|
|
assert data["system"]["current_provider"] == "custom"
|
|
assert data["system"]["current_base_url"] == "http://localhost:4000/v1"
|
|
|
|
cfg_text = (_server_hermes_home() / "config.yaml").read_text(encoding="utf-8")
|
|
env_text = (_server_hermes_home() / ".env").read_text(encoding="utf-8")
|
|
assert "provider: custom" in cfg_text
|
|
assert "default: google/gemma-3-27b-it" in cfg_text
|
|
assert "base_url: http://localhost:4000/v1" in cfg_text
|
|
assert "OPENAI_API_KEY=sk-custom-test" in env_text
|
|
|
|
|
|
@_needs_yaml
|
|
def test_onboarding_setup_detects_incomplete_saved_provider():
|
|
status, code = post(
|
|
"/api/onboarding/setup",
|
|
{
|
|
"provider": "anthropic",
|
|
"model": "claude-sonnet-4.6",
|
|
"api_key": "sk-ant-test",
|
|
},
|
|
)
|
|
assert code == 200
|
|
|
|
(_server_hermes_home() / ".env").unlink(missing_ok=True)
|
|
data, status_code = get("/api/onboarding/status")
|
|
assert status_code == 200
|
|
assert data["system"]["provider_configured"] is True
|
|
assert data["system"]["provider_ready"] is False
|
|
assert data["system"]["chat_ready"] is False
|
|
assert data["system"]["setup_state"] in {"provider_incomplete", "agent_unavailable"}
|
|
|
|
|
|
@_needs_yaml
|
|
def test_onboarding_setup_rejects_missing_custom_base_url():
|
|
data, status = post(
|
|
"/api/onboarding/setup",
|
|
{
|
|
"provider": "custom",
|
|
"model": "qwen2.5-coder",
|
|
"api_key": "sk-test",
|
|
},
|
|
)
|
|
assert status == 400
|
|
assert "base_url is required" in data["error"]
|
|
|
|
|
|
def test_onboarding_complete_persists_flag():
|
|
data, status = post("/api/onboarding/complete", {})
|
|
assert status == 200
|
|
assert data["completed"] is True
|
|
|
|
settings = json.loads(
|
|
(_server_hermes_home() / "settings.json").read_text(encoding="utf-8")
|
|
)
|
|
assert settings["onboarding_completed"] is True
|
|
|
|
data2, status2 = get("/api/onboarding/status")
|
|
assert status2 == 200
|
|
assert data2["completed"] is True
|
|
|
|
|
|
def test_onboarding_complete_preserves_other_settings():
|
|
"""Completing onboarding must not overwrite other user settings."""
|
|
# Use send_key (a safe enum setting) to verify settings preservation
|
|
# without contaminating bot_name or theme checks in other test files.
|
|
# Use GET /api/settings (not onboarding status) to check preservation
|
|
# since the onboarding status only returns a subset of settings fields.
|
|
try:
|
|
saved, s1 = post("/api/settings", {"send_key": "ctrl+enter"})
|
|
assert s1 == 200
|
|
assert saved["send_key"] == "ctrl+enter"
|
|
|
|
_, s2 = post("/api/onboarding/complete", {})
|
|
assert s2 == 200
|
|
|
|
# Verify the non-onboarding setting survived the completion call
|
|
current_settings, s3 = get("/api/settings")
|
|
assert s3 == 200
|
|
assert current_settings["send_key"] == "ctrl+enter"
|
|
finally:
|
|
# Always restore default send_key to avoid contaminating other tests
|
|
post("/api/settings", {"send_key": "enter"})
|
|
|
|
def test_onboarding_already_completed_status():
|
|
"""After marking onboarding complete, status must reflect completed=True
|
|
so the wizard does not re-appear for returning users."""
|
|
done, status = post("/api/onboarding/complete", {})
|
|
assert status == 200
|
|
assert done["completed"] is True
|
|
|
|
data, status2 = get("/api/onboarding/status")
|
|
assert status2 == 200
|
|
assert data["completed"] is True
|
|
|
|
# Reset so test doesn't contaminate others
|
|
post("/api/settings", {"onboarding_completed": False})
|
|
|
|
|
|
@_needs_yaml
|
|
def test_onboarding_setup_rejects_api_key_with_newline():
|
|
"""API keys containing embedded newlines must be rejected to prevent .env injection."""
|
|
injected_key = "sk-bad" + chr(10) + "OTHER_KEY=injected"
|
|
data, status = post(
|
|
"/api/onboarding/setup",
|
|
{
|
|
"provider": "openrouter",
|
|
"model": "anthropic/claude-sonnet-4.6",
|
|
"api_key": injected_key,
|
|
},
|
|
)
|
|
assert status == 400
|
|
assert "newline" in data["error"].lower()
|