fastmcp/tests/scripts/test_release_contributor_credit.py

301 lines
10 KiB
Python

import json
from typing import Any
import pytest
from scripts.release_contributor_credit import fetch_prs, scan_notes, summarize_credit
def actor(login: str, kind: str = "User") -> dict[str, str]:
return {"login": login, "__typename": kind}
def pr_metadata(
number: int,
author: str = "maintainer",
coauthors: tuple[str, ...] = (),
reporters: tuple[str, ...] = (),
) -> dict[str, Any]:
return {
"number": number,
"author": actor(author),
"closingIssuesReferences": {
"nodes": [
{
"number": i,
"url": f"https://github.com/PrefectHQ/fastmcp/issues/{i}",
"author": actor(login),
}
for i, login in enumerate(reporters, 1)
],
"pageInfo": {"hasNextPage": False},
},
"commits": {"nodes": [], "pageInfo": {"hasNextPage": False}},
"mergeCommit": {
"authors": {
"nodes": [
{
"name": login,
"email": f"{login}@users.noreply.github.com",
"user": actor(login),
}
for login in (author, *coauthors)
],
"pageInfo": {"hasNextPage": False},
}
},
}
def entry(number: int, author: str = "maintainer") -> str:
return f"* Fix lookup by @{author} in https://github.com/PrefectHQ/fastmcp/pull/{number}\n"
def mock_github(
monkeypatch: pytest.MonkeyPatch,
prs: list[dict[str, Any]],
permission: str = "write",
maintainers: tuple[str, ...] = ("maintainer",),
) -> list[list[str]]:
calls: list[list[str]] = []
def respond(command: list[str], **kwargs: Any) -> str:
calls.append(command)
if command[2] == "graphql":
return json.dumps(
{"data": {"repository": {f"p{pr['number']}": pr for pr in prs}}}
)
assert command[2].endswith("/permission")
login = command[2].split("/")[-2]
return json.dumps(
{"permission": permission if login in maintainers else "read"}
)
monkeypatch.setattr(
"scripts.release_contributor_credit.subprocess.check_output", respond
)
return calls
def test_batch_credits_missing_reporters_and_reuses_maintainer_permission(
monkeypatch: pytest.MonkeyPatch,
) -> None:
calls = mock_github(
monkeypatch,
[
pr_metadata(1, reporters=("reporter",)),
pr_metadata(2, reporters=("reporter",)),
],
)
result = scan_notes(entry(1) + entry(2))
assert [candidate["credit"] for candidate in result] == [
["maintainer", "reporter"],
["maintainer", "reporter"],
]
assert result[0]["missing_reporter_coauthors"] == ["reporter"]
assert "by @maintainer and @reporter in" in result[0]["suggested_entry"]
assert len(calls) == 3 # Metadata and one permission lookup per distinct account.
def test_human_coauthors_survive_agent_filter_without_an_issue(
monkeypatch: pytest.MonkeyPatch,
) -> None:
pr = pr_metadata(1, coauthors=("helper", "claude", "codex", "automation[bot]"))
pr["mergeCommit"]["authors"]["nodes"].append(
{"name": "Claude Opus 5.5", "email": "noreply@anthropic.com", "user": None}
)
calls = mock_github(monkeypatch, [pr])
result = scan_notes(entry(1))
assert result[0]["credit"] == ["maintainer", "helper"]
assert result[0]["unmapped_commit_authors"] == []
assert len(calls) == 2
def test_external_pr_does_not_automatically_credit_distinct_issue_author(
monkeypatch: pytest.MonkeyPatch,
) -> None:
mock_github(
monkeypatch,
[pr_metadata(1, author="contributor", reporters=("reporter",))],
permission="read",
)
assert scan_notes(entry(1, "contributor")) == []
def test_deduplicates_reporters_and_renders_oxford_comma(
monkeypatch: pytest.MonkeyPatch,
) -> None:
mock_github(
monkeypatch,
[
pr_metadata(
1, coauthors=("helper",), reporters=("helper", "reporter", "reporter")
)
],
)
result = scan_notes(entry(1))
assert result[0]["credit"] == ["maintainer", "helper", "reporter"]
assert "by @maintainer, @helper, and @reporter in" in result[0]["suggested_entry"]
def test_unmapped_human_author_is_flagged_without_guessing_a_handle(
monkeypatch: pytest.MonkeyPatch,
) -> None:
pr = pr_metadata(1)
pr["mergeCommit"]["authors"]["nodes"].append(
{"name": "Helpful Person", "email": "person@example.com", "user": None}
)
mock_github(monkeypatch, [pr])
result = scan_notes(entry(1))
assert result[0]["unmapped_commit_authors"] == ["Helpful Person"]
assert result[0]["credit"] == ["maintainer"]
def test_changed_notes_format_fails_visibly(monkeypatch: pytest.MonkeyPatch) -> None:
calls = mock_github(monkeypatch, [])
with pytest.raises(ValueError, match="Cannot parse release entries"):
scan_notes("* Fix lookup (https://github.com/PrefectHQ/fastmcp/pull/1)")
assert calls == []
@pytest.mark.parametrize("connection", ["issues", "authors"])
def test_truncated_metadata_fails_visibly(
monkeypatch: pytest.MonkeyPatch, connection: str
) -> None:
pr = pr_metadata(1)
nodes = (
pr["closingIssuesReferences"]
if connection == "issues"
else pr["mergeCommit"]["authors"]
)
nodes["pageInfo"]["hasNextPage"] = True
mock_github(monkeypatch, [pr])
with pytest.raises(ValueError, match="truncated"):
fetch_prs([1])
def test_bot_authored_generated_entry_is_parsed_without_crediting_bot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
mock_github(monkeypatch, [pr_metadata(1, author="dependabot[bot]")])
assert scan_notes(entry(1, "dependabot[bot]")) == []
def test_single_human_coauthor_of_bot_pr_is_credited(
monkeypatch: pytest.MonkeyPatch,
) -> None:
mock_github(
monkeypatch, [pr_metadata(1, author="automation[bot]", coauthors=("helper",))]
)
result = scan_notes(entry(1, "automation[bot]"))
assert result[0]["credit"] == ["helper"]
assert "by @helper in" in result[0]["suggested_entry"]
def test_summary_counts_unique_humans_and_flags_first_time_candidates() -> None:
notes = (
entry(1)
+ entry(2)
+ entry(3, "dependabot[bot]")
+ "\n## New Contributors\n"
+ "* @maintainer made their first contribution in https://github.com/PrefectHQ/fastmcp/pull/1\n"
+ "* @claude made their first contribution in https://github.com/PrefectHQ/fastmcp/pull/2\n"
)
candidates = [
{"credit": ["maintainer", "reporter"]},
{"credit": ["Maintainer", "reporter", "helper"]},
]
prs = {
1: pr_metadata(1, reporters=("reporter",)),
2: pr_metadata(2, coauthors=("helper", "claude")),
3: pr_metadata(3, author="dependabot[bot]"),
}
result = summarize_credit(notes, candidates, prs)
assert result["contributors"] == ["helper", "maintainer", "reporter"]
assert result["total_contributors"] == 3
assert result["generated_first_time_contributors"] == ["maintainer"]
assert result["generated_first_time_count"] == 1
assert result["first_time_review"] == ["helper", "maintainer", "reporter"]
def test_maintainer_report_and_coauthor_need_no_supplemental_credit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
mock_github(
monkeypatch,
[
pr_metadata(
1, coauthors=("othermaintainer",), reporters=("othermaintainer",)
)
],
maintainers=("maintainer", "othermaintainer"),
)
assert scan_notes(entry(1)) == []
def test_existing_maintainer_credit_is_preserved(
monkeypatch: pytest.MonkeyPatch,
) -> None:
calls = mock_github(
monkeypatch,
[pr_metadata(1, coauthors=("othermaintainer",))],
maintainers=("maintainer", "othermaintainer"),
)
notes = entry(1).replace(
"by @maintainer in", "by @maintainer and @othermaintainer in"
)
assert scan_notes(notes) == []
assert len(calls) == 1
@pytest.mark.parametrize("merge_strategy", ["merge", "rebase", "squash"])
def test_credits_authors_from_earlier_pr_commits(
monkeypatch: pytest.MonkeyPatch, merge_strategy: str
) -> None:
pr = pr_metadata(1)
earlier = pr_metadata(1, author="helper", coauthors=("reporter", "claude"))
pr["commits"]["nodes"] = [{"commit": earlier["mergeCommit"]}]
if merge_strategy == "squash":
pr["mergeCommit"] = pr_metadata(1, coauthors=("helper", "reporter"))[
"mergeCommit"
]
calls = mock_github(monkeypatch, [pr])
result = scan_notes(entry(1))
assert "commits(first:100)" in calls[0][-1]
assert result[0]["credit"] == ["maintainer", "helper", "reporter"]
@pytest.mark.parametrize("nested", [False, True])
def test_truncated_pr_commit_metadata_fails_visibly(
monkeypatch: pytest.MonkeyPatch, nested: bool
) -> None:
pr = pr_metadata(1)
earlier = pr_metadata(1, author="helper")
pr["commits"]["nodes"] = [{"commit": earlier["mergeCommit"]}]
connection = earlier["mergeCommit"]["authors"] if nested else pr["commits"]
connection["pageInfo"]["hasNextPage"] = True
mock_github(monkeypatch, [pr])
with pytest.raises(ValueError, match="truncated"):
fetch_prs([1])
@pytest.mark.parametrize("login", ["dependabot", "prefect-renovate"])
def test_plain_app_handles_are_excluded_from_all_counts(
monkeypatch: pytest.MonkeyPatch, login: str
) -> None:
pr = pr_metadata(1, author=f"{login}[bot]")
pr["author"] = actor(f"{login}[bot]", "Bot")
pr["mergeCommit"]["authors"]["nodes"][0]["user"] = pr["author"]
notes = (
entry(1, login)
+ f"* @{login} made their first contribution in https://github.com/PrefectHQ/fastmcp/pull/1\n"
)
calls = mock_github(monkeypatch, [pr])
prs = fetch_prs([1])
candidates = scan_notes(notes, prs)
result = summarize_credit(notes, candidates, prs)
assert result["contributors"] == []
assert result["total_contributors"] == 0
assert result["generated_first_time_count"] == 0
assert result["first_time_review"] == []
assert len(calls) == 1