Files
bytedance--deer-flow/backend/tests/test_github_agents_config.py
2026-07-13 11:59:58 +08:00

222 lines
7.8 KiB
Python

"""Tests for the GitHub binding block on :class:`AgentConfig`.
Verifies the new ``github:`` block parses correctly when present, is ``None``
when absent (so every existing agent continues to load unchanged), and that
``load_agent_config`` round-trips through YAML correctly.
"""
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
from deerflow.config.agents_config import (
AgentConfig,
GitHubAgentConfig,
GitHubBinding,
GitHubTriggerConfig,
load_agent_config,
)
def test_github_field_defaults_to_none() -> None:
cfg = AgentConfig(name="solo")
assert cfg.github is None
def test_github_block_parses_full_shape() -> None:
cfg = AgentConfig(
name="coding-llm-gateway",
github={
"installation_id": 123456,
"bindings": [
{
"repo": "zhfeng/llm-gateway",
"triggers": {
"pull_request": {"actions": ["opened", "reopened"]},
"issue_comment": {
"require_mention": True,
"allow_authors": ["zhfeng"],
"mention_login": "coding-llm-gateway-bot",
},
},
}
],
},
)
assert isinstance(cfg.github, GitHubAgentConfig)
assert cfg.github.installation_id == 123456
assert len(cfg.github.bindings) == 1
binding = cfg.github.bindings[0]
assert isinstance(binding, GitHubBinding)
assert binding.repo == "zhfeng/llm-gateway"
assert set(binding.triggers.keys()) == {"pull_request", "issue_comment"}
pr = binding.triggers["pull_request"]
assert isinstance(pr, GitHubTriggerConfig)
assert pr.actions == ["opened", "reopened"]
assert pr.require_mention is False
assert pr.allow_authors == []
comment = binding.triggers["issue_comment"]
assert comment.require_mention is True
assert comment.allow_authors == ["zhfeng"]
assert comment.mention_login == "coding-llm-gateway-bot"
def test_github_block_minimal_uses_defaults() -> None:
cfg = AgentConfig(name="x", github={})
assert cfg.github is not None
assert cfg.github.installation_id is None
assert cfg.github.bindings == []
# recursion_limit defaults to None — the channel default (250) is
# applied later by ChannelManager._resolve_run_params, not here.
assert cfg.github.recursion_limit is None
def test_github_recursion_limit_parses() -> None:
cfg = AgentConfig(name="refactorer", github={"recursion_limit": 500})
assert cfg.github is not None
assert cfg.github.recursion_limit == 500
def test_github_trigger_invalid_type_raises() -> None:
with pytest.raises(Exception): # noqa: PT011 — pydantic ValidationError subclasses Exception
AgentConfig(
name="x",
github={"bindings": [{"repo": "a/b", "triggers": {"pull_request": "not-an-object"}}]},
)
def test_github_bindings_must_have_repo() -> None:
with pytest.raises(Exception): # noqa: PT011
AgentConfig(name="x", github={"bindings": [{"triggers": {}}]})
# ---------------------------------------------------------------------------
# YAML round-trip via load_agent_config
# ---------------------------------------------------------------------------
def _write_agent(base: Path, user_id: str, name: str, body: dict) -> None:
agent_dir = base / "users" / user_id / "agents" / name
agent_dir.mkdir(parents=True, exist_ok=True)
(agent_dir / "config.yaml").write_text(yaml.safe_dump(body), encoding="utf-8")
def test_load_agent_config_reads_github_block(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
# Reset the singleton so the new HOME is picked up.
from deerflow.config import paths as paths_module
monkeypatch.setattr(paths_module, "_paths", None)
body = {
"name": "coding-llm-gateway",
"model": "claude-sonnet-4-6",
"github": {
"installation_id": 999,
"bindings": [
{
"repo": "zhfeng/llm-gateway",
"triggers": {
"pull_request": {"actions": ["opened"]},
},
}
],
},
}
_write_agent(tmp_path, "default", "coding-llm-gateway", body)
cfg = load_agent_config("coding-llm-gateway", user_id="default")
assert cfg is not None
assert cfg.github is not None
assert cfg.github.installation_id == 999
assert cfg.github.bindings[0].repo == "zhfeng/llm-gateway"
assert cfg.github.bindings[0].triggers["pull_request"].actions == ["opened"]
def test_load_agent_config_without_github_block_is_none(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
from deerflow.config import paths as paths_module
monkeypatch.setattr(paths_module, "_paths", None)
_write_agent(tmp_path, "default", "plain-agent", {"name": "plain-agent"})
cfg = load_agent_config("plain-agent", user_id="default")
assert cfg is not None
assert cfg.github is None
# ---------------------------------------------------------------------------
# Single-binding-per-repo validator (PR feedback R3)
# ---------------------------------------------------------------------------
def test_duplicate_bindings_same_repo_rejected() -> None:
"""Two bindings on the same repo must fail validation.
Pre-R3 the dispatcher silently picked the FIRST binding for the repo,
so a second binding with a different ``triggers:`` map would never fire
its events. Fail loudly at config load instead.
"""
with pytest.raises(ValueError, match="duplicate repos"):
GitHubAgentConfig(
bindings=[
GitHubBinding(repo="a/b", triggers={"pull_request": GitHubTriggerConfig()}),
GitHubBinding(repo="a/b", triggers={"issue_comment": GitHubTriggerConfig()}),
],
)
def test_duplicate_bindings_error_lists_offending_repo() -> None:
"""The error mentions every duplicate repo so operators can locate them."""
with pytest.raises(ValueError) as excinfo:
GitHubAgentConfig(
bindings=[
GitHubBinding(repo="x/one"),
GitHubBinding(repo="x/two"),
GitHubBinding(repo="x/one"),
GitHubBinding(repo="x/two"),
],
)
msg = str(excinfo.value)
assert "x/one" in msg
assert "x/two" in msg
def test_distinct_repo_bindings_allowed() -> None:
"""One agent with bindings on different repos is fine (the common case)."""
cfg = GitHubAgentConfig(
bindings=[
GitHubBinding(repo="a/one"),
GitHubBinding(repo="a/two"),
GitHubBinding(repo="b/three"),
],
)
assert [b.repo for b in cfg.bindings] == ["a/one", "a/two", "b/three"]
def test_load_agent_config_rejects_duplicate_repo_bindings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""End-to-end: load_agent_config surfaces the validator error from YAML."""
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
from deerflow.config import paths as paths_module
monkeypatch.setattr(paths_module, "_paths", None)
agent_dir = tmp_path / "users" / "default" / "agents" / "broken"
agent_dir.mkdir(parents=True)
body = {
"name": "broken",
"github": {
"bindings": [
{"repo": "a/b", "triggers": {"pull_request": {}}},
{"repo": "a/b", "triggers": {"issue_comment": {}}},
],
},
}
(agent_dir / "config.yaml").write_text(yaml.safe_dump(body), encoding="utf-8")
with pytest.raises(ValueError, match="duplicate repos"):
load_agent_config("broken", user_id="default")