#121165 skipped the profile-global pointer for any caller with a task_id, but only a GUI session with a workspace callback and a project path actually moves. CLI/messaging agents and pathless projects got a successful no-op.
229 lines
9.8 KiB
Python
229 lines
9.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from hermes_cli import projects_db as pdb
|
|
from tools import project_tools, terminal_tool
|
|
from tools.registry import registry
|
|
|
|
|
|
def _use_projects_db(monkeypatch, tmp_path):
|
|
db_path = tmp_path / "projects.db"
|
|
original_connect_closing = pdb.connect_closing
|
|
monkeypatch.setattr(pdb, "connect_closing", lambda: original_connect_closing(db_path=db_path))
|
|
return db_path, original_connect_closing
|
|
|
|
|
|
def test_agent_session_project_create_keeps_profile_active_pointer_and_moves_session(monkeypatch, tmp_path):
|
|
db_path, connect_closing = _use_projects_db(monkeypatch, tmp_path)
|
|
with connect_closing(db_path=db_path) as conn:
|
|
active_id = pdb.create_project(conn, name="Foreground", folders=["/work/foreground"])
|
|
pdb.set_active(conn, active_id)
|
|
|
|
workspace_moves: list[tuple[str, str, str]] = []
|
|
project_tools.set_project_workspace_callback(lambda *args: workspace_moves.append(args))
|
|
|
|
try:
|
|
result = json.loads(
|
|
project_tools.project_create(
|
|
"Background", path="/work/background", task_id="session-background"
|
|
)
|
|
)
|
|
finally:
|
|
project_tools.set_project_workspace_callback(None)
|
|
|
|
with connect_closing(db_path=db_path) as conn:
|
|
assert pdb.get_active_id(conn) == active_id
|
|
created = pdb.get_project(conn, result["id"])
|
|
|
|
assert result["success"] is True
|
|
assert created is not None
|
|
assert created.name == "Background"
|
|
assert workspace_moves == [("session-background", "/work/background", "Background")]
|
|
|
|
|
|
def test_legacy_project_create_sets_profile_active_pointer(monkeypatch, tmp_path):
|
|
db_path, connect_closing = _use_projects_db(monkeypatch, tmp_path)
|
|
with connect_closing(db_path=db_path) as conn:
|
|
previous_id = pdb.create_project(conn, name="Previous", folders=["/work/previous"])
|
|
pdb.set_active(conn, previous_id)
|
|
|
|
result = json.loads(project_tools.project_create("Created", path="/work/created"))
|
|
|
|
with connect_closing(db_path=db_path) as conn:
|
|
assert pdb.get_active_id(conn) == result["id"]
|
|
|
|
assert result["success"] is True
|
|
|
|
|
|
def test_legacy_project_switch_sets_profile_active_pointer(monkeypatch, tmp_path):
|
|
db_path, connect_closing = _use_projects_db(monkeypatch, tmp_path)
|
|
with connect_closing(db_path=db_path) as conn:
|
|
active_id = pdb.create_project(conn, name="Active", folders=["/work/active"])
|
|
target_id = pdb.create_project(conn, name="Target", folders=["/work/target"])
|
|
pdb.set_active(conn, active_id)
|
|
|
|
result = json.loads(project_tools.project_switch(target_id))
|
|
|
|
with connect_closing(db_path=db_path) as conn:
|
|
assert pdb.get_active_id(conn) == target_id
|
|
|
|
assert result["success"] is True
|
|
assert result["id"] == target_id
|
|
|
|
|
|
def test_agent_session_project_switch_keeps_profile_active_pointer(monkeypatch, tmp_path):
|
|
db_path = tmp_path / "projects.db"
|
|
original_connect_closing = pdb.connect_closing
|
|
|
|
with original_connect_closing(db_path=db_path) as conn:
|
|
active_id = pdb.create_project(conn, name="Foreground", folders=["/work/foreground"])
|
|
background_id = pdb.create_project(conn, name="Background", folders=["/work/background"])
|
|
pdb.set_active(conn, active_id)
|
|
|
|
monkeypatch.setattr(pdb, "connect_closing", lambda: original_connect_closing(db_path=db_path))
|
|
workspace_moves: list[tuple[str, str, str]] = []
|
|
project_tools.set_project_workspace_callback(lambda *args: workspace_moves.append(args))
|
|
|
|
try:
|
|
result = json.loads(project_tools.project_switch(background_id, task_id="session-background"))
|
|
finally:
|
|
project_tools.set_project_workspace_callback(None)
|
|
|
|
with original_connect_closing(db_path=db_path) as conn:
|
|
assert pdb.get_active_id(conn) == active_id
|
|
|
|
assert result["success"] is True
|
|
assert result["id"] == background_id
|
|
assert workspace_moves == [("session-background", "/work/background", "Background")]
|
|
|
|
|
|
def test_session_without_workspace_callback_switch_sets_profile_active_pointer(monkeypatch, tmp_path):
|
|
"""CLI/messaging agents pass a task_id but have no GUI workspace to move — the pointer is the switch."""
|
|
db_path, connect_closing = _use_projects_db(monkeypatch, tmp_path)
|
|
with connect_closing(db_path=db_path) as conn:
|
|
active_id = pdb.create_project(conn, name="Active", folders=["/work/active"])
|
|
target_id = pdb.create_project(conn, name="Target", folders=["/work/target"])
|
|
pdb.set_active(conn, active_id)
|
|
|
|
result = json.loads(project_tools.project_switch(target_id, task_id="cli-session"))
|
|
|
|
with connect_closing(db_path=db_path) as conn:
|
|
assert pdb.get_active_id(conn) == target_id
|
|
assert result["success"] is True
|
|
|
|
|
|
def test_session_switch_to_pathless_project_sets_profile_active_pointer(monkeypatch, tmp_path):
|
|
db_path, connect_closing = _use_projects_db(monkeypatch, tmp_path)
|
|
with connect_closing(db_path=db_path) as conn:
|
|
active_id = pdb.create_project(conn, name="Active", folders=["/work/active"])
|
|
pathless_id = pdb.create_project(conn, name="Pathless")
|
|
pdb.set_active(conn, active_id)
|
|
|
|
workspace_moves: list[tuple[str, str, str]] = []
|
|
project_tools.set_project_workspace_callback(lambda *args: workspace_moves.append(args))
|
|
try:
|
|
json.loads(project_tools.project_switch(pathless_id, task_id="session-a"))
|
|
finally:
|
|
project_tools.set_project_workspace_callback(None)
|
|
|
|
with connect_closing(db_path=db_path) as conn:
|
|
assert pdb.get_active_id(conn) == pathless_id
|
|
assert workspace_moves == []
|
|
|
|
|
|
@pytest.fixture
|
|
def gui_sessions(monkeypatch):
|
|
"""Live GUI sessions wired through the real gateway workspace callback."""
|
|
monkeypatch.setattr("hermes_cli.banner.prefetch_update_check", lambda: None)
|
|
from tui_gateway import server
|
|
|
|
monkeypatch.setattr(server, "_sessions", {})
|
|
monkeypatch.setattr(terminal_tool, "_task_env_overrides", {})
|
|
monkeypatch.setattr(server, "_emit", lambda *a, **k: None)
|
|
monkeypatch.setattr(server, "_persist_session_cwd_and_schedule_git_meta", lambda *a, **k: None)
|
|
project_tools.set_project_workspace_callback(server._apply_project_workspace)
|
|
|
|
def open_session(key, cwd=""):
|
|
session = {"session_key": key, "cwd": cwd, "source": "desktop"}
|
|
server._sessions[f"sid-{key}"] = session
|
|
server._register_session_cwd(session)
|
|
return session
|
|
|
|
try:
|
|
yield open_session
|
|
finally:
|
|
project_tools.set_project_workspace_callback(None)
|
|
|
|
|
|
def _tool(action, task_id=None, **args):
|
|
return json.loads(registry.dispatch("desktop_project", {"action": action, **args}, task_id=task_id))
|
|
|
|
|
|
def test_two_sessions_switch_projects_independently(gui_sessions, tmp_path):
|
|
folders = {name: tmp_path / name for name in ("foreground", "a", "b")}
|
|
for folder in folders.values():
|
|
folder.mkdir()
|
|
with pdb.connect_closing() as conn:
|
|
foreground = pdb.create_project(conn, name="Foreground", folders=[str(folders["foreground"])])
|
|
pdb.set_active(conn, foreground)
|
|
tab_a = gui_sessions("tab-a", str(folders["foreground"]))
|
|
tab_b = gui_sessions("tab-b", str(folders["foreground"]))
|
|
|
|
project_a = _tool("create", "tab-a", name="Project A", path=str(folders["a"]))["id"]
|
|
project_b = _tool("create", "tab-b", name="Project B", path=str(folders["b"]))["id"]
|
|
|
|
# Each tab's workspace moved; the profile-global pointer the sidebar owns did not.
|
|
assert (tab_a["cwd"], tab_b["cwd"]) == (str(folders["a"]), str(folders["b"]))
|
|
with pdb.connect_closing() as conn:
|
|
assert pdb.get_active_id(conn) == foreground
|
|
listed_a, listed_b = _tool("list", "tab-a"), _tool("list", "tab-b")
|
|
assert listed_a["active_id"] == project_a
|
|
assert listed_b["active_id"] == project_b
|
|
assert [p["id"] for p in listed_a["projects"] if p["active"]] == [project_a]
|
|
|
|
# Tab B switching into A's project leaves tab A (and the global pointer) where they were.
|
|
assert _tool("switch", "tab-b", name="Project A")["id"] == project_a
|
|
assert _tool("list", "tab-b")["active_id"] == project_a
|
|
assert _tool("list", "tab-a")["active_id"] == project_a
|
|
assert _tool("switch", "tab-a", name="Foreground")["id"] == foreground
|
|
assert _tool("list", "tab-a")["active_id"] == foreground
|
|
assert _tool("list", "tab-b")["active_id"] == project_a
|
|
with pdb.connect_closing() as conn:
|
|
assert pdb.get_active_id(conn) == foreground
|
|
|
|
|
|
def test_project_list_reports_no_project_for_a_detached_session(gui_sessions, tmp_path):
|
|
with pdb.connect_closing() as conn:
|
|
active = pdb.create_project(conn, name="Sidebar pick", folders=[str(tmp_path)])
|
|
pdb.set_active(conn, active)
|
|
gui_sessions("detached")
|
|
|
|
listed = _tool("list", "detached")
|
|
|
|
assert listed["active_id"] is None
|
|
assert not any(p["active"] for p in listed["projects"])
|
|
# A caller the gateway never registered keeps reading the profile-global pointer.
|
|
assert _tool("list")["active_id"] == active
|
|
|
|
|
|
def test_idempotent_create_from_a_session_keeps_profile_active_pointer(gui_sessions, tmp_path):
|
|
folders = {name: tmp_path / name for name in ("foreground", "existing")}
|
|
for folder in folders.values():
|
|
folder.mkdir()
|
|
with pdb.connect_closing() as conn:
|
|
foreground = pdb.create_project(conn, name="Foreground", folders=[str(folders["foreground"])])
|
|
existing = pdb.create_project(
|
|
conn, name="Existing", folders=[str(folders["existing"])], primary_path=str(folders["existing"]))
|
|
pdb.set_active(conn, foreground)
|
|
tab = gui_sessions("tab", str(folders["foreground"]))
|
|
|
|
result = _tool("create", "tab", name="Again", path=str(folders["existing"]))
|
|
|
|
assert result["id"] == existing
|
|
assert tab["cwd"] == str(folders["existing"])
|
|
with pdb.connect_closing() as conn:
|
|
assert pdb.get_active_id(conn) == foreground
|