Ctrl K
ComunidadGuías…

tests/github/test_compose_notes.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
from __future__ import annotations

import json
import re
import runpy
import subprocess
import sys
from pathlib import Path

import pytest

SCRIPT = Path(__file__).resolve().parents[2] / ".github/scripts/changelog/compose_notes.py"
compose_notes = runpy.run_path(str(SCRIPT))["compose_notes"]
REPO = "o/o"
CREDITS = [
    {"pr": pr, "author": author, "author_url": f"https://github.com/{author}"}
    for pr, author in [(1, "alice"), (2, "bob"), (3, "carol"), (4, "dave"), (5, "bob")]
]


def _raw(highlights: str) -> str:
    return f"<!-- RELEASE_NOTES -->\n{highlights}\n<!-- /RELEASE_NOTES -->"


@pytest.mark.parametrize(
    "highlights",
    [
        "## Major new features\n\n- New feature (#3, @carol)\n\n"
        + "## Breaking changes\n\n- Renamed flag (#4, @dave)\n\n"
        + "## Bug fixes\n\n- Fixed crashes (#1, @alice)",
        "## Major new features\n\n- New feature (#3, @carol)\n\n"
        + "## Bug fixes\n\n- Fixed crashes (#1, @alice)",
        "## Bug fixes\n\n- Fixed crashes (#1, @alice)",
    ],
)
def test_curated_sections_omit_other_contributions(highlights: str) -> None:
    notes = compose_notes(_raw(highlights), CREDITS, REPO)
    highlighted = set(re.findall(r"#(\d+)", highlights))
    summary, thanks = notes.split("### 💜 Thanks to our community")
    assert re.findall(r"(?m)^## .+$", summary) == re.findall(r"(?m)^## .+$", highlights)
    assert summary.count("- ") == highlights.count("- ")
    for credit in CREDITS:
        link = f"[#{credit['pr']}](https://github.com/o/o/pull/{credit['pr']})"
        assert notes.count(link) == int(str(credit["pr"]) in highlighted)
        assert (link in summary) == (str(credit["pr"]) in highlighted)
        assert f"[@{credit['author']}]({credit['author_url']})" in thanks
    assert "/pull/" not in thanks
    assert "## Other contributions" not in notes
    assert "## All contributions" not in notes
    assert notes.endswith("Full Changelog: https://github.com/o/o/blob/main/CHANGELOG.md\n")


@pytest.mark.parametrize(
    "raw",
    [
        "",
        "Agent failed",
        _raw(""),
        _raw("## Highlights\n- Wrong heading (#1)"),
        _raw("## Bug fixes\n## Major new features\n- Wrong order (#1)"),
        _raw("## Bug fixes\n## Bug fixes\n- Duplicate headings (#1)"),
        _raw("## Bug fixes\n- Unknown PR (#999)"),
        _raw("## Bug fixes\n- Missing PR citation"),
        _raw("## Bug fixes\n- Fixed (#1)\n\n## Other contributions\n- Other (#2)"),
    ],
)
def test_unavailable_or_invalid_highlights_keep_every_credit(raw: str) -> None:
    assert compose_notes(raw, CREDITS, REPO) == compose_notes("", CREDITS, REPO)
    notes = compose_notes(raw, CREDITS, REPO)
    assert notes.startswith("### 💜 Thanks to our community\n")
    assert "Other contributions" not in notes
    assert "/pull/" not in notes
    for credit in CREDITS:
        assert f"[@{credit['author']}]({credit['author_url']})" in notes


def test_highlight_credits_use_github_authors_and_deduplicate() -> None:
    notes = compose_notes(_raw("## Bug fixes\n- Fixed issues (#2, #5, #2, @wrong)"), CREDITS, REPO)
    summary, thanks = notes.split("### 💜 Thanks to our community")
    assert "@wrong" not in notes
    assert summary.count("[@bob]") == 1
    assert summary.count("/pull/2)") == 1
    assert summary.count("/pull/5)") == 1
    assert "/pull/" not in thanks


def test_all_highlighted_omits_empty_other_section() -> None:
    notes = compose_notes(_raw("## Bug fixes\n- Fixed (#1)"), CREDITS[:1], REPO)
    assert "Other contributions" not in notes
    assert "Full Changelog:" in notes


@pytest.mark.parametrize("raw", ["", _raw("## Bug fixes\n- Fixed (#3)")])
def test_community_handles_bots_and_deleted_authors(raw: str) -> None:
    credits = [
        {"pr": 3, "author": "", "author_url": ""},
        {"pr": 2, "author": "bot[bot]", "author_url": "https://github.com/apps/bot"},
        {"pr": 1, "author": "bot[bot]", "author_url": "https://github.com/apps/bot"},
    ]
    notes = compose_notes(raw, credits, REPO)
    assert "Author unavailable" not in notes
    assert "Other contributions" not in notes
    assert "/pull/1)" not in notes and "/pull/2)" not in notes
    assert ("/pull/3)" in notes) == bool(raw)
    assert notes.count("[@bot[bot]](https://github.com/apps/bot)") == 1


@pytest.mark.parametrize("raw", ["", _raw("## Bug fixes\n- Fixed (#1, #2, #3, #4, #5)")])
def test_community_credits_all_authors_once_in_alphabetical_order(raw: str) -> None:
    credits = [
        {"pr": pr, "author": author, "author_url": url}
        for pr, author, url in [
            (1, "zed", "https://github.com/zed"),
            (2, "alice", "https://github.com/alice"),
            (3, "Bob", "https://github.com/Bob"),
            (4, "ALICE", "https://github.com/ALICE"),
            (5, "", ""),
            (6, "bot[bot]", "https://github.com/apps/bot"),
        ]
    ]
    notes = compose_notes(raw, credits, REPO)
    assert notes.count("### 💜 Thanks to our community") == 1
    thanks, footer = notes.split("### 💜 Thanks to our community\n\n")[1].split(
        "\n\nFull Changelog:"
    )
    assert thanks.split("\n\n")[-1] == (
        "[@ALICE](https://github.com/ALICE), [@Bob](https://github.com/Bob), "
        "[@bot[bot]](https://github.com/apps/bot), [@zed](https://github.com/zed)"
    )
    assert "Author unavailable" not in thanks
    assert footer.strip() == "https://github.com/o/o/blob/main/CHANGELOG.md"


def test_community_note_without_known_authors() -> None:
    notes = compose_notes("", [{"pr": 1, "author": "", "author_url": ""}], REPO)
    thanks = notes.split("### 💜 Thanks to our community\n\n")[1]
    assert "Thank you for building omnigent with us" in thanks
    assert "[@" not in thanks
    assert "Author unavailable" not in thanks


def test_empty_release_keeps_community_note_and_changelog_link() -> None:
    notes = compose_notes("", [], REPO)
    assert notes.startswith("### 💜 Thanks to our community\n")
    assert "Other contributions" not in notes
    assert "[@" not in notes
    assert notes.endswith("Full Changelog: https://github.com/o/o/blob/main/CHANGELOG.md\n")


def test_cli_missing_highlights_uses_community_fallback(tmp_path: Path) -> None:
    credits = tmp_path / "credits.json"
    credits.write_text(json.dumps(CREDITS))
    out = tmp_path / "notes.md"
    result = subprocess.run(
        [
            sys.executable,
            str(SCRIPT),
            "--highlights",
            str(tmp_path / "missing.txt"),
            "--credits",
            str(credits),
            "--repo",
            REPO,
            "--out",
            str(out),
        ],
        check=True,
        capture_output=True,
        text=True,
    )
    assert out.read_text() == compose_notes("", CREDITS, REPO)
    assert (
        "::warning::Release highlights omitted: drafter output is empty or unavailable"
        in result.stderr
    )


@pytest.mark.parametrize(
    ("raw", "reason"),
    [
        ("Agent failed", "RELEASE_NOTES markers are missing"),
        (_raw(""), "the RELEASE_NOTES block is empty"),
        (_raw("## Highlights\n- Fix (#1)"), "highlight headings"),
        (_raw("## Bug fixes\n- Fixed (#1)\n  wrapped text"), "highlight line 3"),
        (
            _raw("## Bug fixes\n- Fixed (#1) with more text"),
            "must be a bullet ending in PR citations",
        ),
        (_raw("## Bug fixes\n- Fixed (#999)"), "cites a PR outside the release"),
        (_raw("## Bug fixes"), "contains no highlight bullets"),
    ],
)
def test_fallback_warning_explains_reason_and_preserves_credits(raw, reason, capsys) -> None:
    notes = compose_notes(raw, CREDITS, REPO, warn_on_fallback=True)
    assert notes == compose_notes("", CREDITS, REPO)
    warning = capsys.readouterr().err
    assert "::warning::Release highlights omitted:" in warning
    assert reason in warning
    assert "Keeping community thanks and the full changelog link" in warning


def test_valid_highlights_and_intentional_fallback_do_not_warn(capsys) -> None:
    compose_notes(_raw("## Bug fixes\n- Fixed (#1)"), CREDITS, REPO, warn_on_fallback=True)
    compose_notes("", CREDITS, REPO)
    assert capsys.readouterr().err == ""


def test_repeated_pr_across_highlights_falls_back_with_reason(capsys) -> None:
    raw = _raw("## Major new features\n- Feature (#1)\n## Bug fixes\n- Fix (#1)")
    assert compose_notes(raw, CREDITS, REPO, warn_on_fallback=True) == compose_notes(
        "", CREDITS, REPO
    )
    assert "repeats an already-cited PR" in capsys.readouterr().err


def test_cli_invalid_utf8_preserves_community_fallback(tmp_path: Path) -> None:
    credits = tmp_path / "credits.json"
    credits.write_text(json.dumps(CREDITS))
    highlights = tmp_path / "highlights.txt"
    highlights.write_bytes(b"\xff")
    out = tmp_path / "notes.md"
    result = subprocess.run(
        [
            sys.executable,
            str(SCRIPT),
            "--highlights",
            str(highlights),
            "--credits",
            str(credits),
            "--repo",
            REPO,
            "--out",
            str(out),
        ],
        capture_output=True,
        text=True,
        check=True,
    )
    assert "RELEASE_NOTES markers are missing" in result.stderr
    assert out.read_text() == compose_notes("", CREDITS, REPO)