|
|
|
@@ -591,6 +591,222 @@ class TestNewEndpoints:
|
|
|
|
|
resp = self.client.get("/api/cron/jobs/nonexistent-id")
|
|
|
|
|
assert resp.status_code == 404
|
|
|
|
|
|
|
|
|
|
# --- Profiles ---
|
|
|
|
|
|
|
|
|
|
def test_profiles_list_includes_default(self):
|
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
get_hermes_home().mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
resp = self.client.get("/api/profiles")
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
names = [p["name"] for p in resp.json()["profiles"]]
|
|
|
|
|
assert "default" in names
|
|
|
|
|
|
|
|
|
|
def test_profiles_list_falls_back_when_profile_listing_fails(self, monkeypatch):
|
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
import hermes_cli.profiles as profiles_mod
|
|
|
|
|
|
|
|
|
|
hermes_home = get_hermes_home()
|
|
|
|
|
hermes_home.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
(hermes_home / "config.yaml").write_text(
|
|
|
|
|
"model:\n provider: openrouter\n name: anthropic/claude-sonnet-4.6\n",
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
named = hermes_home / "profiles" / "multi-agent"
|
|
|
|
|
named.mkdir(parents=True)
|
|
|
|
|
(named / ".env").write_text("EXAMPLE=1\n", encoding="utf-8")
|
|
|
|
|
(named / "skills" / "demo").mkdir(parents=True)
|
|
|
|
|
(named / "skills" / "demo" / "SKILL.md").write_text("---\nname: demo\n---\n", encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
profiles_mod,
|
|
|
|
|
"list_profiles",
|
|
|
|
|
lambda: (_ for _ in ()).throw(RuntimeError("boom")),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
resp = self.client.get("/api/profiles")
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
profiles = {p["name"]: p for p in resp.json()["profiles"]}
|
|
|
|
|
assert profiles["default"]["is_default"] is True
|
|
|
|
|
assert profiles["default"]["provider"] == "openrouter"
|
|
|
|
|
assert profiles["multi-agent"]["has_env"] is True
|
|
|
|
|
assert profiles["multi-agent"]["skill_count"] == 1
|
|
|
|
|
|
|
|
|
|
def test_profiles_create_rename_delete_round_trip(self, monkeypatch):
|
|
|
|
|
# Stub gateway service teardown so the test doesn't shell out to
|
|
|
|
|
# launchctl/systemctl on the host.
|
|
|
|
|
import hermes_cli.profiles as profiles_mod
|
|
|
|
|
monkeypatch.setattr(profiles_mod, "_cleanup_gateway_service", lambda *a, **kw: None)
|
|
|
|
|
|
|
|
|
|
created = self.client.post("/api/profiles", json={"name": "test-prof"})
|
|
|
|
|
assert created.status_code == 200
|
|
|
|
|
|
|
|
|
|
renamed = self.client.patch(
|
|
|
|
|
"/api/profiles/test-prof",
|
|
|
|
|
json={"new_name": "test-prof-2"},
|
|
|
|
|
)
|
|
|
|
|
assert renamed.status_code == 200
|
|
|
|
|
|
|
|
|
|
names = [p["name"] for p in self.client.get("/api/profiles").json()["profiles"]]
|
|
|
|
|
assert "test-prof" not in names
|
|
|
|
|
assert "test-prof-2" in names
|
|
|
|
|
|
|
|
|
|
deleted = self.client.delete("/api/profiles/test-prof-2")
|
|
|
|
|
assert deleted.status_code == 200
|
|
|
|
|
names = [p["name"] for p in self.client.get("/api/profiles").json()["profiles"]]
|
|
|
|
|
assert "test-prof-2" not in names
|
|
|
|
|
|
|
|
|
|
def test_profile_setup_command_uses_named_profile_wrapper(self):
|
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
|
|
|
|
|
(get_hermes_home() / "profiles" / "coder").mkdir(parents=True)
|
|
|
|
|
|
|
|
|
|
resp = self.client.get("/api/profiles/coder/setup-command")
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
assert resp.json()["command"] == "coder setup"
|
|
|
|
|
|
|
|
|
|
def test_profile_setup_command_uses_hermes_for_default_profile(self):
|
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
|
|
|
|
|
get_hermes_home().mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
resp = self.client.get("/api/profiles/default/setup-command")
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
assert resp.json()["command"] == "hermes setup"
|
|
|
|
|
|
|
|
|
|
def test_profiles_create_creates_wrapper_alias_when_safe(self, monkeypatch, tmp_path):
|
|
|
|
|
import hermes_cli.profiles as profiles_mod
|
|
|
|
|
|
|
|
|
|
wrapper_dir = tmp_path / "bin"
|
|
|
|
|
wrapper_dir.mkdir()
|
|
|
|
|
monkeypatch.setattr(profiles_mod, "_get_wrapper_dir", lambda: wrapper_dir)
|
|
|
|
|
|
|
|
|
|
resp = self.client.post(
|
|
|
|
|
"/api/profiles",
|
|
|
|
|
json={"name": "writer", "clone_from_default": False},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
wrapper_path = wrapper_dir / "writer"
|
|
|
|
|
assert wrapper_path.exists()
|
|
|
|
|
assert wrapper_path.read_text() == '#!/bin/sh\nexec hermes -p writer "$@"\n'
|
|
|
|
|
|
|
|
|
|
def test_profiles_create_with_clone_from_default_copies_default_skills(self, monkeypatch):
|
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
import hermes_cli.profiles as profiles_mod
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(profiles_mod, "create_wrapper_script", lambda name: None)
|
|
|
|
|
default_skill = get_hermes_home() / "skills" / "custom" / "new-skill"
|
|
|
|
|
default_skill.mkdir(parents=True)
|
|
|
|
|
(default_skill / "SKILL.md").write_text("---\nname: new-skill\n---\n", encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
resp = self.client.post(
|
|
|
|
|
"/api/profiles",
|
|
|
|
|
json={"name": "cloned", "clone_from_default": True},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
cloned_skill = get_hermes_home() / "profiles" / "cloned" / "skills" / "custom" / "new-skill" / "SKILL.md"
|
|
|
|
|
assert cloned_skill.exists()
|
|
|
|
|
profiles = {p["name"]: p for p in self.client.get("/api/profiles").json()["profiles"]}
|
|
|
|
|
assert profiles["cloned"]["skill_count"] == 1
|
|
|
|
|
|
|
|
|
|
def test_profiles_create_without_clone_seeds_bundled_skills(self, monkeypatch):
|
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
import hermes_cli.profiles as profiles_mod
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(profiles_mod, "create_wrapper_script", lambda name: None)
|
|
|
|
|
|
|
|
|
|
def fake_seed(profile_dir, quiet=False):
|
|
|
|
|
skill_dir = profile_dir / "skills" / "software-development" / "plan"
|
|
|
|
|
skill_dir.mkdir(parents=True)
|
|
|
|
|
(skill_dir / "SKILL.md").write_text("---\nname: plan\n---\n", encoding="utf-8")
|
|
|
|
|
return {"copied": ["plan"]}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(profiles_mod, "seed_profile_skills", fake_seed)
|
|
|
|
|
|
|
|
|
|
resp = self.client.post(
|
|
|
|
|
"/api/profiles",
|
|
|
|
|
json={"name": "fresh", "clone_from_default": False},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
seeded_skill = get_hermes_home() / "profiles" / "fresh" / "skills" / "software-development" / "plan" / "SKILL.md"
|
|
|
|
|
assert seeded_skill.exists()
|
|
|
|
|
profiles = {p["name"]: p for p in self.client.get("/api/profiles").json()["profiles"]}
|
|
|
|
|
assert profiles["fresh"]["skill_count"] == 1
|
|
|
|
|
|
|
|
|
|
def test_profile_open_terminal_uses_macos_terminal(self, monkeypatch):
|
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
import hermes_cli.web_server as web_server
|
|
|
|
|
|
|
|
|
|
(get_hermes_home() / "profiles" / "coder").mkdir(parents=True)
|
|
|
|
|
calls = []
|
|
|
|
|
monkeypatch.setattr(web_server.sys, "platform", "darwin")
|
|
|
|
|
monkeypatch.setattr(web_server.subprocess, "Popen", lambda args, **kwargs: calls.append(args))
|
|
|
|
|
|
|
|
|
|
resp = self.client.post("/api/profiles/coder/open-terminal")
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
assert calls
|
|
|
|
|
assert calls[0][0] == "osascript"
|
|
|
|
|
assert "coder setup" in " ".join(calls[0])
|
|
|
|
|
|
|
|
|
|
def test_profile_open_terminal_uses_windows_cmd(self, monkeypatch):
|
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
import hermes_cli.web_server as web_server
|
|
|
|
|
|
|
|
|
|
(get_hermes_home() / "profiles" / "coder").mkdir(parents=True)
|
|
|
|
|
calls = []
|
|
|
|
|
monkeypatch.setattr(web_server.sys, "platform", "win32")
|
|
|
|
|
monkeypatch.setattr(web_server.subprocess, "Popen", lambda args, **kwargs: calls.append(args))
|
|
|
|
|
|
|
|
|
|
resp = self.client.post("/api/profiles/coder/open-terminal")
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
assert calls
|
|
|
|
|
assert calls[0][:4] == ["cmd.exe", "/c", "start", ""]
|
|
|
|
|
assert calls[0][-1] == "coder setup"
|
|
|
|
|
|
|
|
|
|
def test_profiles_create_rejects_invalid_name(self):
|
|
|
|
|
resp = self.client.post("/api/profiles", json={"name": "Has Spaces"})
|
|
|
|
|
assert resp.status_code == 400
|
|
|
|
|
|
|
|
|
|
def test_profiles_delete_default_forbidden(self):
|
|
|
|
|
resp = self.client.delete("/api/profiles/default")
|
|
|
|
|
assert resp.status_code == 400
|
|
|
|
|
|
|
|
|
|
def test_profiles_delete_not_found(self):
|
|
|
|
|
resp = self.client.delete("/api/profiles/does-not-exist")
|
|
|
|
|
assert resp.status_code == 404
|
|
|
|
|
|
|
|
|
|
def test_profile_soul_round_trip(self, monkeypatch):
|
|
|
|
|
import hermes_cli.profiles as profiles_mod
|
|
|
|
|
monkeypatch.setattr(profiles_mod, "_cleanup_gateway_service", lambda *a, **kw: None)
|
|
|
|
|
|
|
|
|
|
self.client.post("/api/profiles", json={"name": "soul-prof"})
|
|
|
|
|
get1 = self.client.get("/api/profiles/soul-prof/soul")
|
|
|
|
|
assert get1.status_code == 200
|
|
|
|
|
assert get1.json()["exists"] is True
|
|
|
|
|
|
|
|
|
|
put = self.client.put(
|
|
|
|
|
"/api/profiles/soul-prof/soul",
|
|
|
|
|
json={"content": "# Edited soul"},
|
|
|
|
|
)
|
|
|
|
|
assert put.status_code == 200
|
|
|
|
|
|
|
|
|
|
got = self.client.get("/api/profiles/soul-prof/soul").json()
|
|
|
|
|
assert got["content"] == "# Edited soul"
|
|
|
|
|
|
|
|
|
|
self.client.delete("/api/profiles/soul-prof")
|
|
|
|
|
|
|
|
|
|
def test_profile_soul_unknown_profile_404(self):
|
|
|
|
|
resp = self.client.get("/api/profiles/nonexistent/soul")
|
|
|
|
|
assert resp.status_code == 404
|
|
|
|
|
|
|
|
|
|
def test_skills_list(self):
|
|
|
|
|
resp = self.client.get("/api/skills")
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|