deer-flow/tests/skills/test_github_deep_research.py
Dragon ze 4ff0b0bff1
fix(github-research): resolve the tree against the repository's default branch (#7074)
* fix(github-research): resolve the tree against the repository's default branch

The tree command asked for "main" and, when that was missing, for "master".
A repository whose default branch is neither gets a name-guessing fallback, and
when the stale "master" still resolves GitHub returns it happily: the agent is
handed the tree of an abandoned branch and reports it as the current structure.

Ask for HEAD instead, which GitHub resolves to the repository's own default
branch, so no candidate names are guessed and no extra request is added.

* test(github-research): clarify stale branch tree comment

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-19 11:38:13 +08:00

173 lines
5.4 KiB
Python

"""Exercise the GitHub research client's HTTP transports without network IO."""
import http.client
import io
import runpy
import sys
import urllib.request
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
import pytest
@pytest.fixture
def fallback_client(monkeypatch):
monkeypatch.setitem(sys.modules, "requests", None)
script = (
Path(__file__).resolve().parents[2]
/ "skills/public/github-deep-research/scripts/github_api.py"
)
module = runpy.run_path(str(script))
captured = {}
def fake_urlopen(request, timeout):
# Validate the request target as the real HTTP transport does, without IO.
connection = http.client.HTTPSConnection("api.github.com")
try:
connection.putrequest("GET", request.selector)
finally:
connection.close()
captured.update(url=request.full_url, timeout=timeout, headers=request.headers)
response = io.BytesIO(b'{"ok": true}')
response.status = 200
return response
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
return module["GitHubAPI"](), captured
@pytest.fixture(params=["requests", "urllib"])
def file_content_client(request, monkeypatch):
if request.param == "urllib":
return request.getfixturevalue("fallback_client")
requests = pytest.importorskip("requests")
script = (
Path(__file__).resolve().parents[2]
/ "skills/public/github-deep-research/scripts/github_api.py"
)
module = runpy.run_path(str(script))
captured = {}
def fake_send(session, prepared_request, **kwargs):
captured["url"] = prepared_request.url
response = requests.Response()
response.status_code = 200
response._content = b'{"ok": true}'
return response
monkeypatch.setattr(requests.Session, "send", fake_send)
return module["GitHubAPI"](), captured
@pytest.mark.parametrize(
("path", "encoded_path"),
[
("docs/C# guide.md", "docs/C%23%20guide.md"),
("docs/query?draft.md", "docs/query%3Fdraft.md"),
("docs/literal%20name.md", "docs/literal%2520name.md"),
("文档/入门 guide.md", "%E6%96%87%E6%A1%A3/%E5%85%A5%E9%97%A8%20guide.md"),
("docs/file name.md", "docs/file%20name.md"),
("src/nested/README.md", "src/nested/README.md"),
],
)
def test_file_content_preserves_repository_path(
file_content_client, path, encoded_path
):
client, captured = file_content_client
assert client.get_file_content("owner", "repo", path) == '{"ok": true}'
url = urlsplit(captured["url"])
assert url.path == f"/repos/owner/repo/contents/{encoded_path}"
assert url.query == ""
assert url.fragment == ""
@pytest.mark.parametrize("query", ["bug fix", "C++ & retry#fragment", "模型请求"])
def test_fallback_search_preserves_query(fallback_client, query):
client, captured = fallback_client
assert client.search_issues("owner", "repo", query) == {"ok": True}
url = urlsplit(captured["url"])
assert url.path == "/search/issues"
assert url.fragment == ""
assert parse_qs(url.query) == {
"q": [f"repo:owner/repo {query}"],
"per_page": ["30"],
}
def test_fallback_issues_preserves_filters(fallback_client):
client, captured = fallback_client
assert client.get_issues(
"owner", "repo", state="open", limit=10, labels="bug,help wanted"
) == {"ok": True}
assert parse_qs(urlsplit(captured["url"]).query) == {
"state": ["open"],
"per_page": ["10"],
"labels": ["bug,help wanted"],
}
def test_fallback_ordinary_parameters(fallback_client):
client, captured = fallback_client
assert client.get_contributors("owner", "repo", limit=10) == {"ok": True}
assert parse_qs(urlsplit(captured["url"]).query) == {"per_page": ["10"]}
assert captured["timeout"] == 30
assert captured["headers"]["User-agent"] == "Deep-Research-Bot/1.0"
def test_fallback_without_parameters(fallback_client):
client, captured = fallback_client
assert client.get_repo_info("owner", "repo") == {"ok": True}
assert captured["url"] == "https://api.github.com/repos/owner/repo"
def test_tree_asks_for_the_repository_head(file_content_client):
client, captured = file_content_client
assert client.get_tree("owner", "repo") == {"ok": True}
url = urlsplit(captured["url"])
assert url.path == "/repos/owner/repo/git/trees/HEAD"
assert parse_qs(url.query) == {"recursive": ["1"]}
def test_tree_keeps_an_explicit_branch(file_content_client):
client, captured = file_content_client
assert client.get_tree("owner", "repo", branch="dev", recursive=False) == {
"ok": True
}
url = urlsplit(captured["url"])
assert url.path == "/repos/owner/repo/git/trees/dev"
assert url.query == ""
def test_tree_does_not_guess_another_branch(monkeypatch, fallback_client):
client, _ = fallback_client
asked: list[str] = []
def fail_after_recording(endpoint, params=None, accept=None):
asked.append(endpoint)
raise RuntimeError("HTTP 404")
monkeypatch.setattr(client, "_get", fail_after_recording)
with pytest.raises(RuntimeError):
client.get_tree("owner", "repo")
# A stale `master` still resolves as a branch, so retrying names here silently
# returns the stale branch's tree instead of failing.
assert asked == ["/repos/owner/repo/git/trees/HEAD"]