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())