mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
fix: make default repository configurable (#1429)
* fix: make default repository configurable Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: preserve dashboard repo-less runs * fix: distinguish explicit repo-less dashboard runs --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
parent
c28b1641f8
commit
449cb5d1a8
14 changed files with 347 additions and 13 deletions
|
|
@ -8,6 +8,7 @@ configuration in one place. Per-repo style prompts live in
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ class TeamSettingsUpdate(BaseModel):
|
|||
default_agent_reasoning_effort: str | None = None
|
||||
default_agent_subagent_model: str | None = None
|
||||
default_agent_subagent_reasoning_effort: str | None = None
|
||||
default_repo: str | None = None
|
||||
default_reviewer_model: str | None = None
|
||||
default_reviewer_reasoning_effort: str | None = None
|
||||
default_reviewer_subagent_model: str | None = None
|
||||
|
|
@ -82,6 +84,21 @@ def _client():
|
|||
return get_client()
|
||||
|
||||
|
||||
def _env_default_repo() -> str | None:
|
||||
owner = os.environ.get("DEFAULT_REPO_OWNER", "").strip()
|
||||
name = os.environ.get("DEFAULT_REPO_NAME", "").strip()
|
||||
return f"{owner}/{name}" if owner and name else None
|
||||
|
||||
|
||||
def _parse_repo(value: object) -> dict[str, str] | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
owner, sep, name = value.strip().partition("/")
|
||||
if not sep or not owner.strip() or not name.strip():
|
||||
return None
|
||||
return {"owner": owner.strip(), "name": name.strip()}
|
||||
|
||||
|
||||
def _default_settings() -> dict[str, Any]:
|
||||
fallback_model, fallback_effort = default_model_pair()
|
||||
return {
|
||||
|
|
@ -95,6 +112,7 @@ def _default_settings() -> dict[str, Any]:
|
|||
"default_agent_reasoning_effort": fallback_effort,
|
||||
"default_agent_subagent_model": fallback_model,
|
||||
"default_agent_subagent_reasoning_effort": fallback_effort,
|
||||
"default_repo": _env_default_repo(),
|
||||
"default_reviewer_model": fallback_model,
|
||||
"default_reviewer_reasoning_effort": fallback_effort,
|
||||
"default_reviewer_subagent_model": fallback_model,
|
||||
|
|
@ -138,6 +156,7 @@ async def upsert_team_settings(update: TeamSettingsUpdate) -> dict[str, Any]:
|
|||
"default_agent_reasoning_effort": update.default_agent_reasoning_effort,
|
||||
"default_agent_subagent_model": update.default_agent_subagent_model,
|
||||
"default_agent_subagent_reasoning_effort": update.default_agent_subagent_reasoning_effort,
|
||||
"default_repo": update.default_repo,
|
||||
"default_reviewer_model": update.default_reviewer_model,
|
||||
"default_reviewer_reasoning_effort": update.default_reviewer_reasoning_effort,
|
||||
"default_reviewer_subagent_model": update.default_reviewer_subagent_model,
|
||||
|
|
@ -148,6 +167,11 @@ async def upsert_team_settings(update: TeamSettingsUpdate) -> dict[str, Any]:
|
|||
return value
|
||||
|
||||
|
||||
async def get_team_default_repo() -> dict[str, str] | None:
|
||||
settings = await get_team_settings()
|
||||
return _parse_repo(settings.get("default_repo"))
|
||||
|
||||
|
||||
async def get_team_default_model(
|
||||
role: Literal["agent", "reviewer"],
|
||||
) -> tuple[str, str]:
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ async def _resolve_run_email(login: str, profile: dict[str, Any]) -> str | None:
|
|||
class ThreadCreateBody(BaseModel):
|
||||
prompt: str = Field(min_length=1, max_length=20_000)
|
||||
repo: str | None = None
|
||||
repo_explicitly_none: bool = False
|
||||
model_id: str | None = None
|
||||
effort: str | None = None
|
||||
|
||||
|
|
@ -269,13 +270,7 @@ async def get_dashboard_thread(
|
|||
|
||||
|
||||
def _resolve_repo_config(repo: str | None) -> dict[str, str]:
|
||||
"""Resolve the run's repo from the request, or ``{}`` when none is given.
|
||||
|
||||
A repo is optional: the agent identifies and clones the target repo from the
|
||||
task itself. The dashboard pre-fills the user's default repo on the client,
|
||||
so the request value is authoritative here — an empty value means an
|
||||
intentionally repo-less run, not "fall back to the saved default".
|
||||
"""
|
||||
"""Resolve the run's repo from the request, or ``{}`` when none is given."""
|
||||
return _parse_repo(repo) or {}
|
||||
|
||||
|
||||
|
|
@ -284,6 +279,7 @@ async def _start_agent_run(
|
|||
*,
|
||||
login: str,
|
||||
repo_config: dict[str, str],
|
||||
repo_explicitly_none: bool = False,
|
||||
prompt: str,
|
||||
title: str | None = None,
|
||||
model_id: str | None = None,
|
||||
|
|
@ -309,6 +305,8 @@ async def _start_agent_run(
|
|||
if has_repo:
|
||||
metadata["repo_owner"] = repo_config["owner"]
|
||||
metadata["repo_name"] = repo_config["name"]
|
||||
elif repo_explicitly_none:
|
||||
metadata["repo_explicitly_none"] = True
|
||||
|
||||
client = langgraph_client()
|
||||
await client.threads.create(thread_id=thread_id, metadata=metadata, if_exists="do_nothing")
|
||||
|
|
@ -323,6 +321,8 @@ async def _start_agent_run(
|
|||
}
|
||||
if has_repo:
|
||||
configurable["repo"] = repo_config
|
||||
elif repo_explicitly_none:
|
||||
configurable["repo_explicitly_none"] = True
|
||||
if chosen_model and chosen_effort:
|
||||
configurable["agent_model_id"] = chosen_model
|
||||
configurable["agent_effort"] = chosen_effort
|
||||
|
|
@ -354,6 +354,7 @@ async def create_dashboard_thread(login: str, body: ThreadCreateBody) -> dict[st
|
|||
thread_id,
|
||||
login=login,
|
||||
repo_config=repo_config,
|
||||
repo_explicitly_none=body.repo_explicitly_none,
|
||||
prompt=body.prompt.strip(),
|
||||
model_id=body.model_id,
|
||||
effort=body.effort,
|
||||
|
|
@ -404,6 +405,8 @@ async def send_dashboard_message(
|
|||
}
|
||||
if owner and name:
|
||||
configurable["repo"] = {"owner": owner, "name": name}
|
||||
elif metadata.get("repo_explicitly_none") is True:
|
||||
configurable["repo_explicitly_none"] = True
|
||||
if chosen_model and chosen_effort:
|
||||
configurable["agent_model_id"] = chosen_model
|
||||
configurable["agent_effort"] = chosen_effort
|
||||
|
|
|
|||
|
|
@ -429,8 +429,15 @@ def construct_system_prompt(
|
|||
linear_issue_number: str = "",
|
||||
triggering_user_identity: CollaboratorIdentity | None = None,
|
||||
create_prs: bool = False,
|
||||
default_repo: dict[str, str] | None = None,
|
||||
) -> str:
|
||||
default_prompt_section = _load_default_prompt()
|
||||
if default_repo and default_repo.get("owner") and default_repo.get("name"):
|
||||
repo_line = (
|
||||
"When a repository is not explicitly mentioned, use "
|
||||
f"`{default_repo['owner']}/{default_repo['name']}`."
|
||||
)
|
||||
default_prompt_section += f"\n\n{repo_line}"
|
||||
# Shell-escape: display names/emails are user-controlled (e.g. O'Connor) and
|
||||
# are embedded in a `git config` command the agent copies verbatim.
|
||||
if triggering_user_identity is not None:
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ from .dashboard.agent_overrides import (
|
|||
)
|
||||
from .dashboard.agent_usage import record_agent_thread_usage
|
||||
from .dashboard.options import DEFAULT_MODEL_ID, SUPPORTED_MODEL_IDS, model_supports_effort
|
||||
from .dashboard.team_settings import get_team_default_model_pair
|
||||
from .dashboard.team_settings import get_team_default_model_pair, get_team_default_repo
|
||||
from .integrations.langsmith import _configure_github_proxy
|
||||
from .middleware import (
|
||||
ModelFallbackMiddleware,
|
||||
|
|
@ -100,6 +100,24 @@ from .utils.sandbox_state import (
|
|||
)
|
||||
|
||||
|
||||
async def _resolve_prompt_default_repo(configurable: dict[str, Any]) -> dict[str, str] | None:
|
||||
repo_config = configurable.get("repo")
|
||||
if isinstance(repo_config, dict):
|
||||
owner = repo_config.get("owner")
|
||||
name = repo_config.get("name")
|
||||
if isinstance(owner, str) and isinstance(name, str):
|
||||
return {"owner": owner, "name": name}
|
||||
|
||||
if configurable.get("repo_explicitly_none") is True:
|
||||
return None
|
||||
|
||||
try:
|
||||
return await get_team_default_repo()
|
||||
except Exception:
|
||||
logger.debug("Failed to load team default repo for prompt", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
async def _start_langsmith_sandbox_if_needed(sandbox_backend: SandboxBackendProtocol) -> None:
|
||||
"""Start a LangSmith sandbox before operations that require it to be running."""
|
||||
if os.getenv("SANDBOX_TYPE", "langsmith") != "langsmith":
|
||||
|
|
@ -541,6 +559,8 @@ async def get_agent(config: RunnableConfig) -> Pregel:
|
|||
except Exception:
|
||||
logger.debug("Failed to record agent usage for thread %s", thread_id, exc_info=True)
|
||||
|
||||
prompt_default_repo = await _resolve_prompt_default_repo(configurable)
|
||||
|
||||
logger.info("Returning agent with sandbox for thread %s", thread_id)
|
||||
main_model = make_model(model_id, **model_kwargs)
|
||||
subagent_model = make_model(subagent_model_id, **subagent_model_kwargs)
|
||||
|
|
@ -552,6 +572,7 @@ async def get_agent(config: RunnableConfig) -> Pregel:
|
|||
linear_issue_number=linear_issue_number,
|
||||
triggering_user_identity=triggering_user_identity,
|
||||
create_prs=always_create_prs,
|
||||
default_repo=prompt_default_repo,
|
||||
),
|
||||
tools=[
|
||||
http_request,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from .dashboard.agent_overrides import (
|
|||
from .dashboard.enabled_repos import is_review_repo_enabled
|
||||
from .dashboard.oauth import build_settings_url
|
||||
from .dashboard.profiles import get_profile, get_valid_access_token, has_access_token_record
|
||||
from .dashboard.team_settings import get_team_settings
|
||||
from .dashboard.team_settings import get_team_default_repo, get_team_settings
|
||||
from .dashboard.user_mappings import (
|
||||
email_for_login,
|
||||
login_for_email,
|
||||
|
|
@ -137,7 +137,7 @@ SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET", "")
|
|||
SLACK_BOT_USER_ID = os.environ.get("SLACK_BOT_USER_ID", "")
|
||||
SLACK_BOT_USERNAME = os.environ.get("SLACK_BOT_USERNAME", "")
|
||||
DEFAULT_REPO_OWNER = os.environ.get("DEFAULT_REPO_OWNER", "langchain-ai")
|
||||
DEFAULT_REPO_NAME = os.environ.get("DEFAULT_REPO_NAME", "langchainplus")
|
||||
DEFAULT_REPO_NAME = os.environ.get("DEFAULT_REPO_NAME", "")
|
||||
SLACK_REPO_OWNER = os.environ.get("SLACK_REPO_OWNER", "") or DEFAULT_REPO_OWNER
|
||||
SLACK_REPO_NAME = os.environ.get("SLACK_REPO_NAME", "") or DEFAULT_REPO_NAME
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ def get_repo_config_from_team_mapping(
|
|||
team_identifier: str, project_name: str = ""
|
||||
) -> dict[str, str]:
|
||||
"""Look up repository configuration from LINEAR_TEAM_TO_REPO mapping."""
|
||||
fallback = {"owner": DEFAULT_REPO_OWNER, "name": DEFAULT_REPO_NAME}
|
||||
fallback = {"owner": DEFAULT_REPO_OWNER, "name": DEFAULT_REPO_NAME} if DEFAULT_REPO_NAME else {}
|
||||
|
||||
if not team_identifier or team_identifier not in LINEAR_TEAM_TO_REPO:
|
||||
return fallback
|
||||
|
|
@ -608,8 +608,14 @@ async def get_slack_repo_config(
|
|||
logger.exception("Failed to apply dashboard default_repo for Slack user")
|
||||
|
||||
if not repo_config:
|
||||
repo_config = await get_team_default_repo()
|
||||
|
||||
if not repo_config and default_owner and default_name:
|
||||
repo_config = {"owner": default_owner, "name": default_name}
|
||||
|
||||
if not repo_config:
|
||||
raise HTTPException(400, "no default repository configured")
|
||||
|
||||
return repo_config
|
||||
|
||||
|
||||
|
|
@ -1305,6 +1311,12 @@ async def linear_webhook( # noqa: PLR0911, PLR0912, PLR0915
|
|||
},
|
||||
)
|
||||
|
||||
if not repo_config:
|
||||
repo_config = await get_team_default_repo()
|
||||
|
||||
if not repo_config:
|
||||
return {"status": "ignored", "reason": "No default repository configured"}
|
||||
|
||||
if not _is_repo_allowed(repo_config):
|
||||
logger.warning(
|
||||
"Rejecting Linear webhook: repo '%s/%s' not in allowlist",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# Default Prompt
|
||||
|
||||
When a repository is not explicitly mentioned, use `langchain-ai/langchainplus`. Always assume the organization is `langchain-ai`.
|
||||
When a repository is not explicitly mentioned, use the repository provided in the run metadata or dashboard settings. Do not assume a hardcoded repository name.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.dashboard import thread_api
|
||||
|
||||
|
||||
|
|
@ -8,7 +13,6 @@ def test_resolve_repo_config_parses_request_repo() -> None:
|
|||
|
||||
|
||||
def test_resolve_repo_config_returns_empty_when_no_repo_given() -> None:
|
||||
# None / blank / malformed all mean an intentionally repo-less run — never an error.
|
||||
assert thread_api._resolve_repo_config(None) == {}
|
||||
assert thread_api._resolve_repo_config("") == {}
|
||||
assert thread_api._resolve_repo_config("not-a-repo") == {}
|
||||
|
|
@ -36,3 +40,116 @@ def test_thread_summary_keeps_repo_when_present() -> None:
|
|||
)
|
||||
assert summary["repo"] == "repo"
|
||||
assert summary["repoFullName"] == "octo/repo"
|
||||
|
||||
|
||||
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_start_agent_run_omits_repo_less_marker_when_repo_unset(
|
||||
dashboard_run_client: _FakeLangGraphClient,
|
||||
) -> None:
|
||||
asyncio.run(
|
||||
thread_api._start_agent_run(
|
||||
"thread-id",
|
||||
login="octo",
|
||||
repo_config={},
|
||||
prompt="do work",
|
||||
)
|
||||
)
|
||||
|
||||
configurable = dashboard_run_client.runs.configurable
|
||||
assert configurable is not None
|
||||
assert "repo_explicitly_none" not in configurable
|
||||
assert "repo" not in configurable
|
||||
|
||||
|
||||
def test_start_agent_run_marks_repo_less_config_when_explicit(
|
||||
dashboard_run_client: _FakeLangGraphClient,
|
||||
) -> None:
|
||||
asyncio.run(
|
||||
thread_api._start_agent_run(
|
||||
"thread-id",
|
||||
login="octo",
|
||||
repo_config={},
|
||||
repo_explicitly_none=True,
|
||||
prompt="do work",
|
||||
)
|
||||
)
|
||||
|
||||
configurable = dashboard_run_client.runs.configurable
|
||||
assert configurable is not None
|
||||
assert configurable["repo_explicitly_none"] is True
|
||||
assert "repo" not in configurable
|
||||
|
||||
|
||||
def test_start_agent_run_omits_repo_less_marker_when_repo_configured(
|
||||
dashboard_run_client: _FakeLangGraphClient,
|
||||
) -> None:
|
||||
asyncio.run(
|
||||
thread_api._start_agent_run(
|
||||
"thread-id",
|
||||
login="octo",
|
||||
repo_config={"owner": "octo", "name": "repo"},
|
||||
prompt="do work",
|
||||
)
|
||||
)
|
||||
|
||||
configurable = dashboard_run_client.runs.configurable
|
||||
assert configurable is not None
|
||||
assert configurable["repo"] == {"owner": "octo", "name": "repo"}
|
||||
assert "repo_explicitly_none" not in configurable
|
||||
|
|
|
|||
|
|
@ -120,3 +120,58 @@ async def test_dashboard_followup_on_busy_thread_queues_dashboard_handoff(
|
|||
|
||||
assert client.threads.updates[0]["source"] == "dashboard"
|
||||
assert queued_messages == [{"text": "continue in web", "source": "dashboard"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dashboard_followup_preserves_explicit_repo_less_thread(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
metadata = {
|
||||
"source": "dashboard",
|
||||
"github_login": "octocat",
|
||||
"repo_explicitly_none": True,
|
||||
}
|
||||
client = _FakeClient(metadata)
|
||||
|
||||
monkeypatch.setattr(thread_api, "langgraph_client", lambda: client)
|
||||
monkeypatch.setattr(thread_api, "is_thread_active", _inactive_thread)
|
||||
monkeypatch.setattr(thread_api, "_ensure_dashboard_github_token", _noop_token_check)
|
||||
monkeypatch.setattr(thread_api, "get_profile", _empty_profile)
|
||||
monkeypatch.setattr(thread_api, "_resolve_run_email", _run_email)
|
||||
|
||||
await thread_api.send_dashboard_message(
|
||||
"thread-1",
|
||||
"octocat",
|
||||
thread_api.ThreadMessageBody(content="continue in web"),
|
||||
)
|
||||
|
||||
run_config = client.runs.created[0]["kwargs"]["config"]["configurable"]
|
||||
assert run_config["repo_explicitly_none"] is True
|
||||
assert "repo" not in run_config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dashboard_followup_without_repo_metadata_allows_team_default(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
metadata = {
|
||||
"source": "dashboard",
|
||||
"github_login": "octocat",
|
||||
}
|
||||
client = _FakeClient(metadata)
|
||||
|
||||
monkeypatch.setattr(thread_api, "langgraph_client", lambda: client)
|
||||
monkeypatch.setattr(thread_api, "is_thread_active", _inactive_thread)
|
||||
monkeypatch.setattr(thread_api, "_ensure_dashboard_github_token", _noop_token_check)
|
||||
monkeypatch.setattr(thread_api, "get_profile", _empty_profile)
|
||||
monkeypatch.setattr(thread_api, "_resolve_run_email", _run_email)
|
||||
|
||||
await thread_api.send_dashboard_message(
|
||||
"thread-1",
|
||||
"octocat",
|
||||
thread_api.ThreadMessageBody(content="continue in web"),
|
||||
)
|
||||
|
||||
run_config = client.runs.created[0]["kwargs"]["config"]["configurable"]
|
||||
assert "repo_explicitly_none" not in run_config
|
||||
assert "repo" not in run_config
|
||||
|
|
|
|||
48
tests/test_prompt_default_repo.py
Normal file
48
tests/test_prompt_default_repo.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from agent import server
|
||||
|
||||
|
||||
def test_resolve_prompt_default_repo_uses_explicit_repo(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def fake_get_team_default_repo() -> dict[str, str] | None:
|
||||
raise AssertionError("team default should not be loaded")
|
||||
|
||||
monkeypatch.setattr(server, "get_team_default_repo", fake_get_team_default_repo)
|
||||
|
||||
repo = asyncio.run(
|
||||
server._resolve_prompt_default_repo({"repo": {"owner": "octo", "name": "repo"}})
|
||||
)
|
||||
|
||||
assert repo == {"owner": "octo", "name": "repo"}
|
||||
|
||||
|
||||
def test_resolve_prompt_default_repo_skips_team_default_for_repo_less_run(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def fake_get_team_default_repo() -> dict[str, str] | None:
|
||||
raise AssertionError("team default should not be loaded")
|
||||
|
||||
monkeypatch.setattr(server, "get_team_default_repo", fake_get_team_default_repo)
|
||||
|
||||
repo = asyncio.run(server._resolve_prompt_default_repo({"repo_explicitly_none": True}))
|
||||
|
||||
assert repo is None
|
||||
|
||||
|
||||
def test_resolve_prompt_default_repo_falls_back_to_team_default(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def fake_get_team_default_repo() -> dict[str, str] | None:
|
||||
return {"owner": "team", "name": "repo"}
|
||||
|
||||
monkeypatch.setattr(server, "get_team_default_repo", fake_get_team_default_repo)
|
||||
|
||||
repo = asyncio.run(server._resolve_prompt_default_repo({}))
|
||||
|
||||
assert repo == {"owner": "team", "name": "repo"}
|
||||
|
|
@ -395,6 +395,24 @@ def test_get_slack_repo_config_applies_profile_default_repo(
|
|||
assert repo == {"owner": "profile-owner", "name": "profile-repo"}
|
||||
|
||||
|
||||
def test_get_slack_repo_config_applies_team_default_repo(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
threads_client = _FakeThreadsClient(thread={"metadata": {}})
|
||||
|
||||
async def fake_get_team_default_repo() -> dict[str, str] | None:
|
||||
return {"owner": "team-owner", "name": "team-repo"}
|
||||
|
||||
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeClient(threads_client))
|
||||
monkeypatch.setattr(webapp, "get_team_default_repo", fake_get_team_default_repo)
|
||||
monkeypatch.setattr(webapp, "SLACK_REPO_NAME", "")
|
||||
monkeypatch.setattr(webapp, "DEFAULT_REPO_NAME", "")
|
||||
|
||||
repo = asyncio.run(webapp.get_slack_repo_config("C123", "1.234"))
|
||||
|
||||
assert repo == {"owner": "team-owner", "name": "team-repo"}
|
||||
|
||||
|
||||
def _setup_slack_mention_fakes(
|
||||
monkeypatch: pytest.MonkeyPatch, captured: dict[str, object]
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export function AgentsHome() {
|
|||
createThread.mutate({
|
||||
prompt,
|
||||
repo,
|
||||
repo_explicitly_none: repoOverride === null,
|
||||
model_id: activeSelection?.modelId ?? null,
|
||||
effort: activeSelection?.effort ?? null,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export type { AgentSchedule, AgentThread, Message }
|
|||
export interface ThreadCreateRequest {
|
||||
prompt: string
|
||||
repo?: string | null
|
||||
repo_explicitly_none?: boolean
|
||||
model_id?: string | null
|
||||
effort?: string | null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export interface TeamSettings {
|
|||
default_agent_reasoning_effort?: string | null;
|
||||
default_agent_subagent_model?: string | null;
|
||||
default_agent_subagent_reasoning_effort?: string | null;
|
||||
default_repo?: string | null;
|
||||
default_reviewer_model?: string | null;
|
||||
default_reviewer_reasoning_effort?: string | null;
|
||||
default_reviewer_subagent_model?: string | null;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
|||
import type { ModelOption, TeamSettings, UserMapping } from "@/lib/api";
|
||||
import { AppShell, SettingsRow, SettingsSection } from "@/components/AppShell";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -156,6 +157,11 @@ function GlobalDefaultsSection({ models }: { models: Array<ModelOption> }) {
|
|||
queryFn: api.getTeamSettings,
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [defaultRepoDraft, setDefaultRepoDraft] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setDefaultRepoDraft(settings.data?.default_repo ?? "");
|
||||
}, [settings.data?.default_repo]);
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: (body: TeamSettings) => api.saveTeamSettings(body),
|
||||
|
|
@ -204,6 +210,26 @@ function GlobalDefaultsSection({ models }: { models: Array<ModelOption> }) {
|
|||
}
|
||||
disabled={!settings.data || save.isPending}
|
||||
/>
|
||||
<SettingsRow
|
||||
label="Default Repository"
|
||||
description="Global fallback used when a run has no explicit repo and the user has no profile default. Use owner/repo."
|
||||
control={
|
||||
<Input
|
||||
className="w-56"
|
||||
placeholder="owner/repo"
|
||||
value={defaultRepoDraft}
|
||||
onChange={(e) => setDefaultRepoDraft(e.target.value)}
|
||||
onBlur={() =>
|
||||
settings.data &&
|
||||
save.mutate({
|
||||
...settings.data,
|
||||
default_repo: defaultRepoDraft.trim() || null,
|
||||
})
|
||||
}
|
||||
disabled={!settings.data || save.isPending}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<RolePicker
|
||||
label="Open SWE Reviewer"
|
||||
description="Model used for PR review runs."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue