diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 26fa685f18..9c1f33ed6a 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -19458,3 +19458,55 @@ def test_persist_live_session_system_prompt_restores_pre_existing_override(tmp_p finally: reset_hermes_home_override(outer_token) assert get_hermes_home_override() is None + + +def test_workspace_move_rehomes_running_session(monkeypatch, tmp_path): + """An explicit Move-to-project must win for a RUNNING session: the stored + row and the live runtime session re-anchor together, never a UI-vs-db + disagreement (#86626).""" + target = "stored-running-session" + new_cwd = tmp_path / "dest-project" + new_cwd.mkdir() + captured = {} + + class FakeDB: + def get_session(self, session_id): + return {"id": session_id} + + def update_session_cwd(self, session_id, cwd, branch=None, root=None, replace_git_meta=True): + captured["row_update"] = (session_id, cwd) + + def close(self): + pass + + import contextlib + + @contextlib.contextmanager + def _fake_db(_params): + yield FakeDB() + + monkeypatch.setattr(server, "_profile_db", _fake_db) + monkeypatch.setattr( + server, + "_git_branch_for_cwd", + lambda cwd: "main", + ) + monkeypatch.setattr( + server, + "_git_common_repo_root_for_cwd", + lambda cwd: str(new_cwd), + ) + + live = {"session_key": target, "running": True, "cwd": str(tmp_path / "old-project")} + server._sessions["live-sid"] = live + monkeypatch.setattr(server, "_register_session_cwd", lambda _session: None) + + res = server._methods["session.workspace.move"]( + "rid", + {"session_key": target, "cwd": str(new_cwd)}, + ) + + assert "error" not in res, res + assert captured["row_update"] == (target, str(new_cwd)) + assert live["cwd"] == str(new_cwd) + assert live.get("explicit_cwd") is True diff --git a/tui_gateway/methods_session.py b/tui_gateway/methods_session.py index cea2e210a7..281a18b53f 100644 --- a/tui_gateway/methods_session.py +++ b/tui_gateway/methods_session.py @@ -920,8 +920,11 @@ def _(rid, params: dict) -> dict: stale ``git_repo_root`` would keep it grouped under the project it left. A live agent bound to the row follows through the runtime path too, so its - terminal/file tools re-anchor immediately; a mid-turn session refuses the - move rather than yanking the workspace out from under its tools. + terminal/file tools re-anchor immediately. An explicit move wins even + mid-turn: refusing a running session made the desktop's "Move to project" + claim success in the UI while ``state.db`` kept the old cwd — two sources + of truth disagreeing (#86626). In-flight tool calls keep the cwd they were + launched with; the NEXT tool call uses the new workspace. """ target = str(params.get("session_key") or "").strip() if not target: @@ -944,8 +947,6 @@ def _(rid, params: dict) -> dict: if sess.get("session_key") == target: live, live_sid = sess, sid break - if live is not None and live.get("running"): - return _err(rid, 4009, "session busy") branch = _git_branch_for_cwd(resolved) root = _git_common_repo_root_for_cwd(resolved)