222 lines
7.8 KiB
Python
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")
|