""" 发布记录的范围、作者、分页与手工摘要保护契约。 """ import json import pytest from scripts import release_notes def make_commit(sha: str, subject: str, login: str | None = "contributor", parents: int = 1) -> dict: """ 构造包含 GitHub 作者及原始 Git 显示名的提交响应。 """ return { "sha": sha, "html_url": f"https://github.com/owner/repo/commit/{sha}", "parents": [{}] * parents, "author": {"login": login} if login else None, "commit": {"message": subject, "author": {"name": "Display Name"}}, } def test_changelog_preserves_subjects_and_distinct_commits() -> None: """ 不分类、翻译或按标题去重,真实账号与未关联作者分别展示。 """ commits = [ make_commit("a" * 40, "fix(scope): 原始标题\n详细正文"), make_commit("b" * 40, "fix(scope): 原始标题", login=None), make_commit("c" * 40, "Merge pull request #1", parents=2), make_commit("d" * 40, "合并提交", parents=2), make_commit("e" * 40, "build(plugin-market): sync default from MoviePilot-Wiki@123"), make_commit("f" * 40, "ci: improve pipeline"), ] body = release_notes.changelog(commits + commits[:1]) assert body.count("fix(scope): 原始标题") == 2 assert "by @contributor" in body assert "by Display Name" in body assert "@Display Name" not in body assert "Merge" not in body and "合并提交" not in body assert "plugin-market" not in body assert "ci: improve pipeline" in body assert "详细正文" not in body def test_previous_tag_excludes_current_future_and_other_major(monkeypatch) -> None: """ 同版本重打包仍从上一正式版计算,不受较高版本或预发布标签影响。 """ monkeypatch.setattr(release_notes, "command", lambda *_: "v2.9.9\nv3.1.2\nv3.1.2-1\nv3.1.3\nv3.1.4\nv3.1.3-beta.1\n") assert release_notes.previous_tag("v3.1.3") == "v3.1.2-1" assert release_notes.previous_tag("v3.1.2-2") == "v3.1.2-1" assert release_notes.previous_tag("v3.0.0") is None def test_collect_commits_includes_every_page(monkeypatch) -> None: """ 跨页发布记录包含直接提交,比较终点使用固定 SHA 而非可移动标签。 """ calls = [] def fake_command(*args): """ 模拟 GitHub Compare API 的多页响应。 """ calls.append(args) return json.dumps([{"commits": [make_commit("a", "first")]}, {"commits": [make_commit("b", "second")]}]) monkeypatch.setattr(release_notes, "command", fake_command) commits = release_notes.collect_commits("owner/repo", "fixed-sha", "v3.1.2") assert [commit["sha"] for commit in commits] == ["a", "b"] assert calls == [("gh", "api", "--paginate", "--slurp", "repos/owner/repo/compare/v3.1.2...fixed-sha?per_page=100")] def test_first_release_collects_entire_history(monkeypatch) -> None: """ 首次发布不凭空选择其它大版本,提交历史按时间正序输出。 """ monkeypatch.setattr(release_notes, "command", lambda *_: json.dumps([[make_commit("b", "new")], [make_commit("a", "old")]])) commits = release_notes.collect_commits("owner/repo", "fixed-sha", None) assert [commit["sha"] for commit in commits] == ["a", "b"] @pytest.mark.parametrize("summary", ["", "- 手工功能说明\n\n### 注意事项\n\n原样保留 `代码` 与 @author", "说明\n\n## 手工子标题\n\n细节"]) def test_rebuild_preserves_manual_summary_and_refreshes_records(summary: str) -> None: """ 重建保留手工摘要,刷新自动章节,正文始终仅包含三个约定章节。 """ old = release_notes.release_body(summary, "old records", "owner/repo", "v3.1.1", "v3.1.2") preserved = release_notes.manual_summary(old) new = release_notes.release_body(preserved, "new records", "owner/repo", "v3.1.1", "v3.1.2") assert preserved == summary assert "old records" not in new assert "new records" in new assert "https://github.com/owner/repo/compare/v3.1.1...v3.1.2" in new assert "贡献者" not in new def test_legacy_manual_body_is_preserved_but_generated_body_is_not_summary() -> None: """ 兼容旧手工公告,避免把原有自动分类记录当作本次用户摘要。 """ assert release_notes.manual_summary("手工公告\n- 新功能") == "手工公告\n- 新功能" assert release_notes.manual_summary("### ✨ 新功能\n\n- generated by @author") == "" assert release_notes.manual_summary("手工公告\n\n### ✨ 新功能\n- generated") == "手工公告" @pytest.mark.parametrize("frontend", [False, True]) def test_main_emits_workflow_body_without_evaluating_commit_text(tmp_path, monkeypatch, frontend) -> None: """ 前端仅输出列表,后端保留手工内容;提交中的工作流分隔符不得截断正文。 """ environment = tmp_path / "github.env" monkeypatch.setenv("GITHUB_ENV", str(environment)) monkeypatch.setenv("RELEASE_BODY", "## 本次更新内容\n\n手工公告\n\n## 完整变更记录\nold") monkeypatch.setattr(release_notes, "previous_tag", lambda _: "v3.1.2") monkeypatch.setattr(release_notes, "collect_commits", lambda *_: [make_commit("a" * 40, "fix: $() EOF")]) args = ["release_notes.py", "--repository", "owner/repo", "--tag", "v3.1.3", "--source", "fixed-sha"] if frontend: args.append("--changelog-only") monkeypatch.setattr("sys.argv", args) release_notes.main() content = environment.read_text(encoding="utf-8") delimiter = content.splitlines()[0].split("<<")[1] assert content.splitlines()[-1] == delimiter assert "fix: $() EOF by @contributor" in content assert ("## 本次更新内容" in content) is not frontend assert ("手工公告" in content) is not frontend assert ("## 版本对比" in content) is not frontend