mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 03:00:23 +00:00
242 lines
7.7 KiB
Python
242 lines
7.7 KiB
Python
import json
|
|
import subprocess
|
|
import time
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
import api.models as models
|
|
from api.models import SESSIONS, Session, new_session
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_sessions(tmp_path, monkeypatch):
|
|
session_dir = tmp_path / "sessions"
|
|
session_dir.mkdir()
|
|
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
|
|
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
|
|
SESSIONS.clear()
|
|
yield session_dir
|
|
SESSIONS.clear()
|
|
|
|
|
|
def test_worktree_metadata_round_trips_through_session_file(_isolate_sessions):
|
|
s = Session(
|
|
session_id="worktree001",
|
|
workspace=str(_isolate_sessions.parent / "repo" / ".worktrees" / "hermes-1234"),
|
|
worktree_path=str(_isolate_sessions.parent / "repo" / ".worktrees" / "hermes-1234"),
|
|
worktree_branch="hermes/hermes-1234",
|
|
worktree_repo_root=str(_isolate_sessions.parent / "repo"),
|
|
worktree_created_at=123.5,
|
|
)
|
|
s.save()
|
|
|
|
raw = json.loads(s.path.read_text(encoding="utf-8"))
|
|
assert raw["worktree_path"].endswith(".worktrees/hermes-1234")
|
|
assert raw["worktree_branch"] == "hermes/hermes-1234"
|
|
assert raw["worktree_repo_root"].endswith("repo")
|
|
assert raw["worktree_created_at"] == 123.5
|
|
|
|
loaded = Session.load("worktree001")
|
|
assert loaded.worktree_path == s.worktree_path
|
|
assert loaded.worktree_branch == "hermes/hermes-1234"
|
|
assert loaded.worktree_repo_root == s.worktree_repo_root
|
|
assert loaded.worktree_created_at == 123.5
|
|
assert loaded.compact()["worktree_branch"] == "hermes/hermes-1234"
|
|
|
|
|
|
def test_new_session_with_worktree_info_persists_immediately(_isolate_sessions):
|
|
repo = _isolate_sessions.parent / "repo"
|
|
worktree = repo / ".worktrees" / "hermes-abcd1234"
|
|
worktree.mkdir(parents=True)
|
|
|
|
s = new_session(
|
|
workspace=str(worktree),
|
|
worktree_info={
|
|
"path": str(worktree),
|
|
"branch": "hermes/hermes-abcd1234",
|
|
"repo_root": str(repo),
|
|
"created_at": 456.0,
|
|
},
|
|
)
|
|
|
|
assert s.path.exists(), (
|
|
"worktree-backed sessions must be persisted at creation time so the "
|
|
"real filesystem worktree is not orphaned by a browser/server restart"
|
|
)
|
|
assert s.worktree_path == str(worktree.resolve())
|
|
assert s.worktree_branch == "hermes/hermes-abcd1234"
|
|
assert s.worktree_repo_root == str(repo.resolve())
|
|
assert s.worktree_created_at == 456.0
|
|
|
|
|
|
def test_empty_worktree_session_remains_visible_in_sidebar(_isolate_sessions):
|
|
repo = _isolate_sessions.parent / "repo"
|
|
worktree = repo / ".worktrees" / "hermes-visible"
|
|
worktree.mkdir(parents=True)
|
|
|
|
s = new_session(
|
|
workspace=str(worktree),
|
|
worktree_info={
|
|
"path": str(worktree),
|
|
"branch": "hermes/hermes-visible",
|
|
"repo_root": str(repo),
|
|
"created_at": 789.0,
|
|
},
|
|
)
|
|
|
|
ids = {row["session_id"] for row in models.all_sessions()}
|
|
assert s.session_id in ids, (
|
|
"worktree-backed sessions represent real filesystem state immediately "
|
|
"and must survive the empty-session sidebar filter"
|
|
)
|
|
|
|
|
|
def test_find_git_repo_root_uses_git_from_nested_workspace(tmp_path):
|
|
from api.worktrees import find_git_repo_root
|
|
|
|
repo = tmp_path / "repo"
|
|
nested = repo / "apps" / "web"
|
|
nested.mkdir(parents=True)
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
|
|
assert find_git_repo_root(nested) == repo.resolve()
|
|
|
|
|
|
def test_find_git_repo_root_rejects_non_git_workspace(tmp_path):
|
|
from api.worktrees import find_git_repo_root
|
|
|
|
with pytest.raises(ValueError, match="not inside a git repository"):
|
|
find_git_repo_root(tmp_path)
|
|
|
|
|
|
def test_create_worktree_for_workspace_calls_agent_setup_with_repo_root(tmp_path, monkeypatch):
|
|
import api.worktrees as worktrees
|
|
|
|
repo = tmp_path / "repo"
|
|
nested = repo / "src"
|
|
nested.mkdir(parents=True)
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
seen = {}
|
|
|
|
def fake_setup(repo_root):
|
|
seen["repo_root"] = repo_root
|
|
return {
|
|
"path": str(repo / ".worktrees" / "hermes-test"),
|
|
"branch": "hermes/hermes-test",
|
|
"repo_root": str(repo),
|
|
}
|
|
|
|
monkeypatch.setattr(worktrees, "_setup_agent_worktree", fake_setup)
|
|
now = time.time()
|
|
|
|
info = worktrees.create_worktree_for_workspace(nested)
|
|
|
|
assert seen["repo_root"] == str(repo.resolve())
|
|
assert info["path"].endswith(".worktrees/hermes-test")
|
|
assert info["branch"] == "hermes/hermes-test"
|
|
assert info["repo_root"] == str(repo.resolve())
|
|
assert info["created_at"] >= now
|
|
|
|
|
|
def test_session_new_route_creates_worktree_backed_session(tmp_path, monkeypatch):
|
|
import api.routes as routes
|
|
import api.worktrees as worktrees
|
|
|
|
repo = tmp_path / "repo"
|
|
worktree = repo / ".worktrees" / "hermes-route"
|
|
repo.mkdir()
|
|
worktree.mkdir(parents=True)
|
|
|
|
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"read_body",
|
|
lambda handler: {
|
|
"workspace": str(repo),
|
|
"worktree": True,
|
|
"profile": "default",
|
|
},
|
|
)
|
|
monkeypatch.setattr(routes, "resolve_trusted_workspace", lambda raw: repo if raw == str(repo) else raw)
|
|
monkeypatch.setattr(
|
|
worktrees,
|
|
"create_worktree_for_workspace",
|
|
lambda workspace: {
|
|
"path": str(worktree),
|
|
"branch": "hermes/hermes-route",
|
|
"repo_root": str(repo),
|
|
"created_at": 321.0,
|
|
},
|
|
)
|
|
captured = {}
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"j",
|
|
lambda handler, payload, status=200, extra_headers=None: captured.update(
|
|
payload=payload,
|
|
status=status,
|
|
) or True,
|
|
)
|
|
|
|
assert routes.handle_post(object(), SimpleNamespace(path="/api/session/new")) is True
|
|
assert captured["status"] == 200
|
|
session = captured["payload"]["session"]
|
|
assert session["workspace"] == str(worktree.resolve())
|
|
assert session["worktree_path"] == str(worktree.resolve())
|
|
assert session["worktree_branch"] == "hermes/hermes-route"
|
|
|
|
|
|
def test_session_new_worktree_fallback_workspace_is_resolved(tmp_path, monkeypatch):
|
|
import api.routes as routes
|
|
import api.worktrees as worktrees
|
|
|
|
repo = tmp_path / "repo"
|
|
worktree = repo / ".worktrees" / "hermes-route"
|
|
repo.mkdir()
|
|
worktree.mkdir(parents=True)
|
|
seen = {"resolved": []}
|
|
|
|
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"read_body",
|
|
lambda handler: {
|
|
"worktree": True,
|
|
"profile": "default",
|
|
},
|
|
)
|
|
monkeypatch.setattr(routes, "get_last_workspace", lambda: str(repo))
|
|
|
|
def fake_resolve(raw):
|
|
seen["resolved"].append(raw)
|
|
return repo
|
|
|
|
monkeypatch.setattr(routes, "resolve_trusted_workspace", fake_resolve)
|
|
monkeypatch.setattr(
|
|
worktrees,
|
|
"create_worktree_for_workspace",
|
|
lambda workspace: {
|
|
"path": str(worktree),
|
|
"branch": "hermes/hermes-route",
|
|
"repo_root": str(repo),
|
|
"created_at": 321.0,
|
|
},
|
|
)
|
|
captured = {}
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"j",
|
|
lambda handler, payload, status=200, extra_headers=None: captured.update(
|
|
payload=payload,
|
|
status=status,
|
|
) or True,
|
|
)
|
|
|
|
assert routes.handle_post(object(), SimpleNamespace(path="/api/session/new")) is True
|
|
|
|
assert seen["resolved"] == [str(repo)]
|
|
assert captured["status"] == 200
|
|
session = captured["payload"]["session"]
|
|
assert session["workspace"] == str(worktree.resolve())
|