* fix(desktop): stabilize project folder sessions Keep desktop folder selection aligned with new sessions and scope TUI gateway cwd through session context so prompts and tools resolve against the selected workspace. * fix(desktop): address review feedback on folder sessions Snapshot sessions before iterating to avoid concurrent-mutation crashes, optional-chain the revealLogs catch, and read console-message args from the correct Electron event/messageDetails positions. * fix(desktop): address second review pass on folder sessions Sync the remembered workspace key with the cwd atom (clear on empty), only load tree children for real directory nodes, and throttle renderer auto-reloads so a deterministic startup crash can't loop forever. * fix(desktop): inherit parent workspace for ephemeral agent tasks Background and preview tasks use ephemeral ids absent from the session map, so pass the parent session cwd into the session context explicitly instead of clearing it back to the gateway launch dir. Also correct the set_session_vars docstring about clear_session_vars semantics. * fix(desktop): validate preview cwd before pinning session context A non-empty but non-existent client cwd would pin an unusable override and silently fall back to the launch dir. Validate once, reuse for both the session context and the terminal override, and fall back to the parent session workspace when invalid. * fix(desktop): harden preview cwd normalization and adopt normalized cwd Guard preview cwd normalization against malformed client paths so a bad input can't fail the whole restart, and adopt the backend's normalized config.get cwd in the no-active-session path so the persisted workspace stays consistent with what the agent uses.
130 lines
5.2 KiB
Python
130 lines
5.2 KiB
Python
"""Tests for agent/runtime_cwd.py — the single source of truth for the agent working directory."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import agent.runtime_cwd as rt
|
|
from agent.runtime_cwd import (
|
|
clear_session_cwd,
|
|
resolve_agent_cwd,
|
|
resolve_context_cwd,
|
|
set_session_cwd,
|
|
)
|
|
|
|
|
|
def _raise_oserror(*args, **kwargs):
|
|
raise OSError("cwd gone")
|
|
|
|
|
|
class TestResolveAgentCwd:
|
|
def test_prefers_terminal_cwd_over_getcwd(self, monkeypatch, tmp_path):
|
|
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
|
monkeypatch.chdir(os.path.expanduser("~"))
|
|
assert resolve_agent_cwd() == tmp_path
|
|
|
|
def test_falls_back_to_getcwd_when_unset(self, monkeypatch, tmp_path):
|
|
# The #19242 local-CLI contract: TERMINAL_CWD is unset, so the launch dir wins.
|
|
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
|
monkeypatch.chdir(tmp_path)
|
|
assert resolve_agent_cwd() == tmp_path
|
|
|
|
def test_skips_nonexistent_terminal_cwd(self, monkeypatch, tmp_path):
|
|
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path / "gone"))
|
|
monkeypatch.chdir(tmp_path)
|
|
assert resolve_agent_cwd() == tmp_path
|
|
|
|
def test_expands_leading_tilde(self, monkeypatch):
|
|
monkeypatch.setenv("TERMINAL_CWD", "~")
|
|
assert resolve_agent_cwd() == Path(os.path.expanduser("~"))
|
|
|
|
def test_whitespace_only_terminal_cwd_falls_back_to_getcwd(self, monkeypatch, tmp_path):
|
|
# " ".strip() → "" → falsy, so the launch dir wins (not a " " path).
|
|
monkeypatch.setenv("TERMINAL_CWD", " ")
|
|
monkeypatch.chdir(tmp_path)
|
|
assert resolve_agent_cwd() == tmp_path
|
|
|
|
def test_propagates_oserror_from_getcwd(self, monkeypatch):
|
|
# The fallback arm calls os.getcwd(), which can raise OSError (deleted cwd).
|
|
# The resolver must NOT swallow it — build_environment_hints owns the
|
|
# try/except OSError guard at the call site (prompt_builder.py:805).
|
|
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
|
monkeypatch.setattr(rt.os, "getcwd", _raise_oserror)
|
|
with pytest.raises(OSError):
|
|
resolve_agent_cwd()
|
|
|
|
|
|
class TestResolveContextCwd:
|
|
def test_returns_dir_when_set(self, monkeypatch, tmp_path):
|
|
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
|
assert resolve_context_cwd() == tmp_path
|
|
|
|
def test_returns_none_when_unset(self, monkeypatch):
|
|
# Unset → None; the caller (build_context_files_prompt) then getcwds —
|
|
# the local-CLI #19242 contract. Discovery still runs; it is NOT skipped.
|
|
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
|
assert resolve_context_cwd() is None
|
|
|
|
def test_returns_nonexistent_dir_unguarded(self, monkeypatch, tmp_path):
|
|
# Deliberate asymmetry vs resolve_agent_cwd: context discovery has no isdir
|
|
# guard, so a missing dir is returned (not None) — discovery just finds nothing.
|
|
missing = tmp_path / "gone"
|
|
monkeypatch.setenv("TERMINAL_CWD", str(missing))
|
|
assert resolve_context_cwd() == missing
|
|
|
|
def test_expands_leading_tilde(self, monkeypatch):
|
|
monkeypatch.setenv("TERMINAL_CWD", "~")
|
|
assert resolve_context_cwd() == Path(os.path.expanduser("~"))
|
|
|
|
def test_whitespace_only_terminal_cwd_returns_none(self, monkeypatch):
|
|
# " ".strip() → "" → None, so the caller getcwds for discovery rather
|
|
# than building Path(" ") and resolving garbage under the launch dir.
|
|
monkeypatch.setenv("TERMINAL_CWD", " ")
|
|
assert resolve_context_cwd() is None
|
|
|
|
|
|
class TestSessionCwdOverride:
|
|
"""The #29531 per-session arm: a contextvar cwd wins over TERMINAL_CWD so a
|
|
multi-session gateway can pin each session to its own folder."""
|
|
|
|
def test_session_cwd_overrides_terminal_cwd(self, monkeypatch, tmp_path):
|
|
other = tmp_path / "other"
|
|
other.mkdir()
|
|
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
|
token = set_session_cwd(str(other))
|
|
try:
|
|
assert resolve_agent_cwd() == other
|
|
assert resolve_context_cwd() == other
|
|
finally:
|
|
rt._SESSION_CWD.reset(token)
|
|
|
|
def test_empty_session_cwd_falls_back_to_terminal_cwd(self, monkeypatch, tmp_path):
|
|
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
|
token = set_session_cwd("")
|
|
try:
|
|
assert resolve_agent_cwd() == tmp_path
|
|
assert resolve_context_cwd() == tmp_path
|
|
finally:
|
|
rt._SESSION_CWD.reset(token)
|
|
|
|
def test_clear_session_cwd_restores_terminal_cwd(self, monkeypatch, tmp_path):
|
|
other = tmp_path / "other"
|
|
other.mkdir()
|
|
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
|
token = set_session_cwd(str(other))
|
|
try:
|
|
clear_session_cwd()
|
|
assert resolve_agent_cwd() == tmp_path
|
|
finally:
|
|
rt._SESSION_CWD.reset(token)
|
|
|
|
def test_nonexistent_session_cwd_falls_back(self, monkeypatch, tmp_path):
|
|
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
|
token = set_session_cwd(str(tmp_path / "gone"))
|
|
try:
|
|
# resolve_agent_cwd guards on isdir; a missing session cwd must not win.
|
|
assert resolve_agent_cwd() == tmp_path
|
|
finally:
|
|
rt._SESSION_CWD.reset(token)
|