mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
* feat(dashboard): stream agent chat via @langchain/react v2 protocol Replace the bespoke SSE + React Query polling path with LangGraph’s v2 event stream through credentialed dashboard proxies. Run starts go through stream commands; mid-run follow-ups still queue via /messages. * fix import path * fix tests after rebase * format * PR feedback * improved model fallback * fix image handling * embrace sdk * cleanup * cr * more cleanup * fix cors * harden security --------- Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
163 lines
5.2 KiB
Python
163 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from agent.dashboard import thread_api
|
|
|
|
|
|
def test_resolve_repo_config_parses_request_repo() -> None:
|
|
assert thread_api._resolve_repo_config("octo/repo") == {"owner": "octo", "name": "repo"}
|
|
|
|
|
|
def test_resolve_repo_config_returns_empty_when_no_repo_given() -> None:
|
|
assert thread_api._resolve_repo_config(None) == {}
|
|
assert thread_api._resolve_repo_config("") == {}
|
|
assert thread_api._resolve_repo_config("not-a-repo") == {}
|
|
|
|
|
|
def test_thread_summary_blanks_repo_when_absent() -> None:
|
|
summary = thread_api._thread_summary(
|
|
{"thread_id": "t1", "metadata": {"source": "dashboard", "title": "no repo run"}}
|
|
)
|
|
assert summary["repo"] == ""
|
|
assert summary["repoFullName"] == ""
|
|
|
|
|
|
def test_thread_summary_keeps_repo_when_present() -> None:
|
|
summary = thread_api._thread_summary(
|
|
{
|
|
"thread_id": "t2",
|
|
"metadata": {
|
|
"source": "dashboard",
|
|
"title": "repo run",
|
|
"repo_owner": "octo",
|
|
"repo_name": "repo",
|
|
},
|
|
}
|
|
)
|
|
assert summary["repo"] == "repo"
|
|
assert summary["repoFullName"] == "octo/repo"
|
|
|
|
|
|
def test_thread_summary_includes_trace_url(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
thread_api,
|
|
"get_langsmith_trace_url",
|
|
lambda thread_id: f"https://smith.example/t/{thread_id}",
|
|
)
|
|
summary = thread_api._thread_summary(
|
|
{"thread_id": "t3", "metadata": {"source": "dashboard", "title": "traced run"}}
|
|
)
|
|
assert summary["traceUrl"] == "https://smith.example/t/t3"
|
|
|
|
|
|
class _FakeThreadsClient:
|
|
async def create(
|
|
self, *, thread_id: str, metadata: dict[str, Any], if_exists: str
|
|
) -> dict[str, Any]:
|
|
return {"thread_id": thread_id, "metadata": metadata, "if_exists": if_exists}
|
|
|
|
async def update(self, *, thread_id: str, metadata: dict[str, Any]) -> dict[str, Any]:
|
|
return {"thread_id": thread_id, "metadata": metadata}
|
|
|
|
async def get(self, thread_id: str) -> dict[str, Any]:
|
|
return {"thread_id": thread_id, "metadata": {}}
|
|
|
|
|
|
class _FakeRunsClient:
|
|
def __init__(self) -> None:
|
|
self.configurable: dict[str, Any] | None = None
|
|
|
|
async def create(
|
|
self,
|
|
thread_id: str,
|
|
assistant_id: str,
|
|
*,
|
|
input: dict[str, Any],
|
|
config: dict[str, Any],
|
|
if_not_exists: str = "reject",
|
|
stream_mode: list[str] | None = None,
|
|
stream_resumable: bool = False,
|
|
) -> dict[str, str]:
|
|
self.configurable = config["configurable"]
|
|
return {"run_id": "run-id"}
|
|
|
|
|
|
class _FakeLangGraphClient:
|
|
def __init__(self) -> None:
|
|
self.threads = _FakeThreadsClient()
|
|
self.runs = _FakeRunsClient()
|
|
|
|
|
|
@pytest.fixture
|
|
def dashboard_run_client(monkeypatch: pytest.MonkeyPatch) -> _FakeLangGraphClient:
|
|
client = _FakeLangGraphClient()
|
|
|
|
async def fake_get_profile(login: str) -> dict[str, Any]:
|
|
return {}
|
|
|
|
async def fake_ensure_token(login: str) -> None:
|
|
return None
|
|
|
|
async def fake_resolve_email(login: str, profile: dict[str, Any]) -> str:
|
|
return "octo@example.com"
|
|
|
|
monkeypatch.setattr(thread_api, "langgraph_client", lambda: client)
|
|
monkeypatch.setattr(thread_api, "get_profile", fake_get_profile)
|
|
monkeypatch.setattr(thread_api, "_ensure_dashboard_github_token", fake_ensure_token)
|
|
monkeypatch.setattr(thread_api, "_resolve_run_email", fake_resolve_email)
|
|
return client
|
|
|
|
|
|
def test_create_thread_record_omits_repo_less_marker_when_repo_unset(
|
|
dashboard_run_client: _FakeLangGraphClient,
|
|
) -> None:
|
|
# Runs now start client-side via the stream commands endpoint, so the run
|
|
# configurable is assembled from thread metadata by
|
|
# ``_build_dashboard_configurable``. The thread record must not persist a
|
|
# repo-less marker when the repo is simply unset (not explicitly cleared).
|
|
asyncio.run(
|
|
thread_api._create_dashboard_thread_record(
|
|
"thread-id",
|
|
login="octo",
|
|
repo_config={},
|
|
prompt="do work",
|
|
)
|
|
)
|
|
|
|
configurable = asyncio.run(
|
|
thread_api._build_dashboard_configurable("thread-id", "octo", {"source": "dashboard"})
|
|
)
|
|
assert "repo_explicitly_none" not in configurable
|
|
assert "repo" not in configurable
|
|
|
|
|
|
def test_build_configurable_marks_repo_less_config_when_explicit(
|
|
dashboard_run_client: _FakeLangGraphClient,
|
|
) -> None:
|
|
configurable = asyncio.run(
|
|
thread_api._build_dashboard_configurable(
|
|
"thread-id",
|
|
"octo",
|
|
{"source": "dashboard", "repo_explicitly_none": True},
|
|
)
|
|
)
|
|
assert configurable["repo_explicitly_none"] is True
|
|
assert "repo" not in configurable
|
|
|
|
|
|
def test_build_configurable_includes_repo_when_configured(
|
|
dashboard_run_client: _FakeLangGraphClient,
|
|
) -> None:
|
|
configurable = asyncio.run(
|
|
thread_api._build_dashboard_configurable(
|
|
"thread-id",
|
|
"octo",
|
|
{"source": "dashboard", "repo_owner": "octo", "repo_name": "repo"},
|
|
)
|
|
)
|
|
assert configurable["repo"] == {"owner": "octo", "name": "repo"}
|
|
assert "repo_explicitly_none" not in configurable
|