From 75db4d7dede216cd0b1940d257cf8e5f64815f66 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 23 Jun 2026 11:55:39 -0700 Subject: [PATCH] [eric] agents: FIX latent NameError on cwd sessions (ensure_cwd_git_repo import mismatch) + clean workspace_git + regression test --- backend/apps/agents/agent_manager.py | 2 +- .../apps/agents/manager/AgentLaunchMixin.py | 4 +- .../agents/manager/session/workspace_git.py | 53 ++++++++++--------- backend/tests/test_streaming_harness.py | 32 +++++++++++ 4 files changed, 64 insertions(+), 27 deletions(-) diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 449b6ef0..d1b9cbc3 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -71,7 +71,7 @@ from backend.apps.agents.manager.MessagingMixin import MessagingMixin from backend.apps.agents.manager.AgentLaunchMixin import AgentLaunchMixin from backend.apps.agents.manager.RunSupportMixin import RunSupportMixin from backend.apps.agents.manager.permissions import gate_hooks -from backend.apps.agents.manager.session.workspace_git import _detect_git_identity as detect_git_identity, _ensure_cwd_git_repo +from backend.apps.agents.manager.session.workspace_git import detect_git_identity, ensure_cwd_git_repo from backend.apps.agents.manager.prompt.tool_catalog import ( FULL_TOOLS, get_all_known_tool_names, diff --git a/backend/apps/agents/manager/AgentLaunchMixin.py b/backend/apps/agents/manager/AgentLaunchMixin.py index c602b2af..77bc8b4c 100644 --- a/backend/apps/agents/manager/AgentLaunchMixin.py +++ b/backend/apps/agents/manager/AgentLaunchMixin.py @@ -21,8 +21,8 @@ from backend.apps.settings.settings import load_settings from backend.apps.agents.manager.session.session_store import _load_session_data as load_session_data from backend.apps.agents.manager.session.apply_context_window import apply_context_window from backend.apps.agents.manager.session.workspace_git import ( - _detect_git_identity as detect_git_identity, - _ensure_cwd_git_repo as ensure_cwd_git_repo, + detect_git_identity, + ensure_cwd_git_repo, ) from backend.apps.agents.manager.prompt.tool_catalog import get_all_tool_names from backend.apps.agents.manager.prompt.prompt_context import resolve_mode diff --git a/backend/apps/agents/manager/session/workspace_git.py b/backend/apps/agents/manager/session/workspace_git.py index c9dd68cb..fd6d376a 100644 --- a/backend/apps/agents/manager/session/workspace_git.py +++ b/backend/apps/agents/manager/session/workspace_git.py @@ -1,10 +1,14 @@ import logging import os +from typing import Optional, Tuple + +from typeguard import typechecked logger = logging.getLogger(__name__) -def _ensure_cwd_git_repo(cwd: str, home: str | None = None) -> None: +@typechecked +def ensure_cwd_git_repo(cwd: str, home: Optional[str] = None) -> None: """Idempotently make `cwd` into a git repo with a valid HEAD. The CLI's built-in Agent tool uses `isolation: "worktree"` to spawn @@ -31,35 +35,35 @@ def _ensure_cwd_git_repo(cwd: str, home: str | None = None) -> None: if not os.path.isdir(cwd): return - import subprocess as _sp_git + import subprocess as sp_git # Case A: cwd is inside some git repo (possibly parent). Verify # HEAD resolves. If the enclosing repo is broken (e.g. a stray # `.git` in $HOME with no commits, which makes workspaces # under ~/.openswarm/workspaces/ inherit a broken HEAD), we # need to init a fresh repo AT cwd so it shadows the parent. - _inside = _sp_git.run( + inside = sp_git.run( ["git", "rev-parse", "--is-inside-work-tree"], cwd=cwd, - stdout=_sp_git.PIPE, stderr=_sp_git.DEVNULL, timeout=5, + stdout=sp_git.PIPE, stderr=sp_git.DEVNULL, timeout=5, ) - if _inside.returncode == 0 and b"true" in _inside.stdout: + if inside.returncode == 0 and b"true" in inside.stdout: # Check HEAD resolves (has at least one commit). - _head = _sp_git.run( + head = sp_git.run( ["git", "rev-parse", "--verify", "HEAD"], cwd=cwd, - stdout=_sp_git.DEVNULL, stderr=_sp_git.DEVNULL, timeout=5, + stdout=sp_git.DEVNULL, stderr=sp_git.DEVNULL, timeout=5, ) - if _head.returncode == 0: + if head.returncode == 0: return # parent repo is healthy, leave it alone # Parent repo exists but HEAD is broken. if os.path.isdir(os.path.join(cwd, ".git")): # .git is directly here, commit to fix it. - _sp_git.run( + sp_git.run( ["git", "-c", "user.email=openswarm@local", "-c", "user.name=OpenSwarm", "commit", "--allow-empty", "-q", "-m", "openswarm init"], cwd=cwd, - stdout=_sp_git.DEVNULL, stderr=_sp_git.DEVNULL, timeout=10, + stdout=sp_git.DEVNULL, stderr=sp_git.DEVNULL, timeout=10, ) return # .git is in a parent dir (broken home-dir repo, etc.). @@ -68,23 +72,24 @@ def _ensure_cwd_git_repo(cwd: str, home: str | None = None) -> None: # Case B: cwd is not a git repo at all (or parent is broken): # init + empty commit here. - _sp_git.run( + sp_git.run( ["git", "init", "-q", "-b", "main"], cwd=cwd, - stdout=_sp_git.DEVNULL, stderr=_sp_git.DEVNULL, timeout=10, + stdout=sp_git.DEVNULL, stderr=sp_git.DEVNULL, timeout=10, ) - _sp_git.run( + sp_git.run( ["git", "-c", "user.email=openswarm@local", "-c", "user.name=OpenSwarm", "commit", "--allow-empty", "-q", "-m", "openswarm init"], cwd=cwd, - stdout=_sp_git.DEVNULL, stderr=_sp_git.DEVNULL, timeout=10, + stdout=sp_git.DEVNULL, stderr=sp_git.DEVNULL, timeout=10, ) - except Exception as _e: - logger.info(f"[agent-cwd] git init skipped: {_e}") + except Exception as exc: + logger.info(f"[agent-cwd] git init skipped: {exc}") -def _detect_git_identity(cwd: str) -> tuple[str | None, str | None]: +@typechecked +def detect_git_identity(cwd: str) -> Tuple[Optional[str], Optional[str]]: """Resolve the origin remote and current branch for `cwd`. Used to label sessions in the session list ("Agent on owner/repo @@ -97,12 +102,12 @@ def _detect_git_identity(cwd: str) -> tuple[str | None, str | None]: if not cwd or not os.path.isdir(cwd): return (None, None) try: - import subprocess as _sp - url_proc = _sp.run( + import subprocess as sp + url_proc = sp.run( ["git", "remote", "get-url", "origin"], - cwd=cwd, stdout=_sp.PIPE, stderr=_sp.DEVNULL, timeout=3, + cwd=cwd, stdout=sp.PIPE, stderr=sp.DEVNULL, timeout=3, ) - repo_url: str | None = None + repo_url: Optional[str] = None if url_proc.returncode == 0: raw = url_proc.stdout.decode("utf-8", errors="replace").strip() if raw: @@ -113,11 +118,11 @@ def _detect_git_identity(cwd: str) -> tuple[str | None, str | None]: repo_url = f"{scheme}://{rest}" else: repo_url = raw - branch_proc = _sp.run( + branch_proc = sp.run( ["git", "branch", "--show-current"], - cwd=cwd, stdout=_sp.PIPE, stderr=_sp.DEVNULL, timeout=3, + cwd=cwd, stdout=sp.PIPE, stderr=sp.DEVNULL, timeout=3, ) - branch_name: str | None = None + branch_name: Optional[str] = None if branch_proc.returncode == 0: raw_b = branch_proc.stdout.decode("utf-8", errors="replace").strip() if raw_b: diff --git a/backend/tests/test_streaming_harness.py b/backend/tests/test_streaming_harness.py index bd11b9f5..b028c623 100644 --- a/backend/tests/test_streaming_harness.py +++ b/backend/tests/test_streaming_harness.py @@ -197,6 +197,38 @@ def test_loop_builds_direct_anthropic_key_env(monkeypatch): assert env == {"ANTHROPIC_API_KEY": "sk-ant-test123"} # direct key, no 9router proxy +def test_loop_with_session_cwd_runs_workspace_git_init(monkeypatch): + # Regression: a session WITH a cwd hits the workspace git-init call in the loop. Harness + # sessions normally have no cwd, which masked a NameError (the call said ensure_cwd_git_repo + # while only _ensure_cwd_git_repo was imported). raising=True here would fail if the name were + # missing again; the assertions confirm the cwd path actually runs and the turn completes. + import backend.apps.agents.agent_manager as am + called = {} + + def fake_ensure(cwd, home=None): + called["cwd"] = cwd + + monkeypatch.setattr(am, "ensure_cwd_git_repo", fake_ensure, raising=True) + + events = [] + + async def fake_send(session_id, event, data): + events.append((event, data)) + + monkeypatch.setattr(ws_mod.ws_manager, "send_to_session", fake_send, raising=True) + monkeypatch.setattr(claude_agent_sdk, "query", _mock_query_yielding( + _assistant([TextBlock(text="done")]), _result()), raising=True) + + mgr = AgentManager() + from backend.apps.agents.core.models import AgentSession + session = AgentSession(name="t", model="sonnet", dashboard_id="d", cwd="/tmp/openswarm-test-ws") + mgr.sessions[session.id] = session + asyncio.run(mgr.p_run_agent_loop(session.id, "hi")) + + assert called.get("cwd") == "/tmp/openswarm-test-ws" # the git-init path ran (no NameError) + assert session.status == "completed" + + def test_full_streaming_turn_drives_the_complete_ws_contract(monkeypatch): # The closest in-repo proxy for a live streaming run: drive the REAL loop with the exact # SDK sequence the live provider emits, partial StreamEvents (block start -> text deltas ->