chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:12:00 +08:00
commit 3de48288cb
2986 changed files with 1131193 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Tests for the PR-template GitHub automation scripts."""
+358
View File
@@ -0,0 +1,358 @@
from __future__ import annotations
import importlib.util
import re
import sys
from pathlib import Path
import pytest
SCRIPT = Path(__file__).resolve().parents[2] / ".github" / "scripts" / "changelog" / "generate.py"
spec = importlib.util.spec_from_file_location("changelog_generate", SCRIPT)
assert spec and spec.loader
gen = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = gen
spec.loader.exec_module(gen)
# --- previous_final_tag ------------------------------------------------------
_TAGS = ["v0.1.0", "v0.1.0rc4", "v0.1.1", "v0.2.0", "v0.2.0rc1", "v0.3.0", "v0.3.0rc1"]
def test_previous_final_tag_for_minor() -> None:
assert gen.previous_final_tag("v0.3.0", _TAGS) == "v0.2.0"
def test_previous_final_tag_for_patch() -> None:
# A patch picks the previous patch/minor, never a higher minor.
assert gen.previous_final_tag("v0.2.1", [*_TAGS, "v0.2.1"]) == "v0.2.0"
def test_previous_final_tag_ignores_prereleases() -> None:
assert gen.previous_final_tag("v0.2.0", _TAGS) == "v0.1.1"
def test_previous_final_tag_first_release() -> None:
assert gen.previous_final_tag("v0.1.0", ["v0.1.0", "v0.1.0rc4"]) is None
def test_previous_final_tag_skips_rc_between_finals() -> None:
# v0.2.0, v0.3.0rc0, v0.3.0 present → previous of v0.3.0 is v0.2.0 (rc ignored).
assert gen.previous_final_tag("v0.3.0", ["v0.2.0", "v0.3.0rc0", "v0.3.0"]) == "v0.2.0"
# --- collect() base override -------------------------------------------------
def _stub_io(monkeypatch, *, subjects: list[str], all_tags: list[str], date: str = "2026-07-02"):
"""Stub the git/gh IO so collect() runs offline; records the range args seen."""
seen: list[tuple[str | None, str]] = []
def fake_range_subjects(prev, tag):
seen.append((prev, tag))
return subjects
monkeypatch.setattr(gen, "_all_tags", lambda: all_tags)
monkeypatch.setattr(gen, "_range_subjects", fake_range_subjects)
monkeypatch.setattr(gen, "_tag_date", lambda tag: date)
monkeypatch.setattr(gen, "_gh_pr_body", lambda repo, pr: None)
_stub_io.seen = seen
def test_collect_uses_base_override_as_range_start(monkeypatch) -> None:
_stub_io(monkeypatch, subjects=[], all_tags=["v0.3.0"])
gen.collect("HEAD", "o/o", base="v0.3.0")
# Range start is the explicit base, NOT previous_final_tag (which would raise
# on the non-version "HEAD").
assert _stub_io.seen == [("v0.3.0", "HEAD")]
def test_collect_without_base_uses_previous_final_tag(monkeypatch) -> None:
_stub_io(monkeypatch, subjects=[], all_tags=["v0.2.0", "v0.3.0rc0", "v0.3.0"])
gen.collect("v0.3.0", "o/o")
assert _stub_io.seen == [("v0.2.0", "v0.3.0")]
# --- CLI guard rail ----------------------------------------------------------
def test_cli_non_version_ref_without_base_errors(monkeypatch, capsys) -> None:
# A non-version ref (branch/sha) can't be ordered and has no default range.
monkeypatch.setattr(sys, "argv", ["generate.py", "--tag", "ci-test", "--repo", "o/o"])
with pytest.raises(SystemExit) as exc:
gen.main()
assert exc.value.code != 0
assert "not a PEP 440 version" in capsys.readouterr().err
# --- pr_numbers_from_subjects ------------------------------------------------
def test_pr_numbers_extracted_and_deduped() -> None:
subjects = [
"feat(web): show progress bar (#1304)",
"fix(policies): reject url policies (#1507)",
"chore: no pr ref here",
"feat(web): show progress bar (#1304)", # duplicate
]
assert gen.pr_numbers_from_subjects(subjects) == [1304, 1507]
def test_pr_titles_strip_ref_and_dedupe() -> None:
subjects = [
"feat(web): show progress bar (#1304)",
"fix(policies): reject url policies (#1507)",
"feat(web): show progress bar again (#1304)", # dup number, first wins
]
titles = gen.pr_titles_from_subjects(subjects)
assert titles == {
1304: "feat(web): show progress bar",
1507: "fix(policies): reject url policies",
}
# --- harvest_pr --------------------------------------------------------------
_TYPE_SECTION = {
"UI / frontend change": "- [x] UI / frontend change\n- [ ] Bug fix\n",
"Bug fix": "- [ ] UI / frontend change\n- [x] Bug fix\n",
"Breaking change": "- [ ] Bug fix\n- [x] Breaking change\n",
"none": "- [ ] Bug fix\n- [ ] Feature\n",
}
def _body(changelog: str | None, kind: str = "Bug fix") -> str:
type_block = _TYPE_SECTION[kind]
changelog_block = "" if changelog is None else f"\n## Changelog\n\n{changelog}\n"
return f"## Summary\n\nThing.\n\n## Type of change\n\n{type_block}{changelog_block}"
def test_harvest_included_with_description_and_tag() -> None:
body = _body("Projects workspace groups sessions", "UI / frontend change")
result = gen.harvest_pr(123, body)
assert result.status == "included"
assert result.description == "Projects workspace groups sessions"
assert result.type_tags == ["UI / frontend change"]
def test_harvest_takes_first_line_of_multiline() -> None:
result = gen.harvest_pr(123, _body("first line of the change\n\nmore detail\nand more"))
assert result.status == "included"
assert result.description == "first line of the change"
def test_harvest_placeholder_is_omitted() -> None:
placeholder = "<Add a line to describe the change, else delete this section>"
result = gen.harvest_pr(123, _body(placeholder))
assert result.status == "omitted"
assert result.description == ""
def test_harvest_deleted_section_is_omitted() -> None:
result = gen.harvest_pr(123, _body(None))
assert result.status == "omitted"
@pytest.mark.parametrize("marker", ["skip", "n/a", "N/A", "none", "-"])
def test_harvest_omit_markers_are_omitted(marker: str) -> None:
# Leftover `skip`/`n/a` (old template sentinel) must not leak in as an entry.
result = gen.harvest_pr(123, _body(marker))
assert result.status == "omitted"
assert result.description == ""
def test_harvest_missing_body() -> None:
assert gen.harvest_pr(123, None).status == "omitted"
# --- render_section ----------------------------------------------------------
def _result(pr: int, description: str, type_tags: list[str] | None = None) -> object:
r = gen.HarvestResult(pr)
r.description = description
r.type_tags = type_tags or []
r.status = "included" if description else "omitted"
return r
def test_render_section_flat_list_with_tags_sorted_by_pr() -> None:
results = [
_result(20, "a crash fix", ["Bug fix"]),
_result(5, "another thing", ["Feature"]),
_result(10, "watch flag", ["UI / frontend change"]),
]
section = gen.render_section("v0.3.0", "2026-06-27", results)
assert section.startswith("## [v0.3.0] — 2026-06-27")
# Flat list, sorted by PR number, each with its bracket tag.
assert section.index("another thing (#5)") < section.index("watch flag (#10)")
assert section.index("watch flag (#10)") < section.index("a crash fix (#20)")
assert "- [UI] watch flag (#10)" in section
assert "- [Bug fix] a crash fix (#20)" in section
# No category sub-headings.
assert "### " not in section
def test_render_section_multiple_tags_join() -> None:
section = gen.render_section(
"v0.3.0", "2026-06-27", [_result(7, "did a thing", ["UI / frontend change", "Bug fix"])]
)
assert "- [UI / Bug fix] did a thing (#7)" in section
def test_render_section_untagged_entry_has_no_bracket() -> None:
section = gen.render_section("v0.3.0", "2026-06-27", [_result(7, "did a thing", [])])
assert "- did a thing (#7)" in section
def test_render_section_omits_undocumented_prs() -> None:
results = [_result(10, "documented", ["Bug fix"]), _result(20, "", [])]
section = gen.render_section("v0.3.0", "2026-06-27", results)
assert "(#10)" in section and "(#20)" not in section
def test_render_section_no_entries() -> None:
section = gen.render_section("v0.3.0", "2026-06-27", [])
assert "_No user-facing changes._" in section
# --- insert_section ----------------------------------------------------------
_SEED = gen._SEED_CHANGELOG
def _section(tag: str, date: str, text: str) -> str:
return f"## [{tag}] — {date}\n\n### Added\n- {text} (#1)\n"
def test_insert_into_empty_then_order_newest_first() -> None:
doc = gen.insert_section(_SEED, "v0.2.0", _section("v0.2.0", "2026-06-19", "two"))
doc = gen.insert_section(doc, "v0.3.0", _section("v0.3.0", "2026-06-27", "three"))
assert doc.index("[v0.3.0]") < doc.index("[v0.2.0]")
# Preamble stays on top.
assert doc.index("# Changelog") < doc.index("[v0.3.0]")
def test_insert_patch_lands_below_newer_minor() -> None:
doc = gen.insert_section(_SEED, "v0.3.0", _section("v0.3.0", "2026-06-27", "three"))
doc = gen.insert_section(doc, "v0.2.0", _section("v0.2.0", "2026-06-19", "two"))
doc = gen.insert_section(doc, "v0.2.1", _section("v0.2.1", "2026-06-30", "patch"))
assert doc.index("[v0.3.0]") < doc.index("[v0.2.1]") < doc.index("[v0.2.0]")
def test_insert_is_idempotent_and_replaces() -> None:
doc = gen.insert_section(_SEED, "v0.3.0", _section("v0.3.0", "2026-06-27", "old"))
doc = gen.insert_section(doc, "v0.3.0", _section("v0.3.0", "2026-06-27", "new"))
assert doc.count("[v0.3.0]") == 1
assert "new (#1)" in doc and "old (#1)" not in doc
def test_insert_prerelease_orders_by_pep440_and_coexists() -> None:
# A dev tag lands above the previous final; the eventual final sorts above the
# dev; an rc sits between them. All three (final/rc/dev) coexist as blocks.
doc = gen.insert_section(_SEED, "v0.3.0", _section("v0.3.0", "2026-06-26", "three"))
doc = gen.insert_section(doc, "v0.4.0dev0", _section("v0.4.0dev0", "2026-07-02", "dev"))
doc = gen.insert_section(doc, "v0.4.0", _section("v0.4.0", "2026-07-10", "final"))
doc = gen.insert_section(doc, "v0.4.0rc1", _section("v0.4.0rc1", "2026-07-08", "rc"))
headers = re.findall(r"(?m)^## \[([^\]]+)\]", doc)
assert headers == ["v0.4.0", "v0.4.0rc1", "v0.4.0dev0", "v0.3.0"]
def test_insert_final_and_dev_are_distinct_blocks() -> None:
# v0.4.0 must NOT overwrite v0.4.0dev0 (no collapse of dev → final).
doc = gen.insert_section(_SEED, "v0.4.0dev0", _section("v0.4.0dev0", "2026-07-02", "dev"))
doc = gen.insert_section(doc, "v0.4.0", _section("v0.4.0", "2026-07-10", "final"))
assert doc.count("[v0.4.0dev0]") == 1
assert re.search(r"(?m)^## \[v0\.4\.0\]", doc) is not None
assert "dev (#1)" in doc and "final (#1)" in doc
def test_insert_dev_tag_idempotent() -> None:
doc = gen.insert_section(_SEED, "v0.4.0dev0", _section("v0.4.0dev0", "2026-07-02", "old"))
doc = gen.insert_section(doc, "v0.4.0dev0", _section("v0.4.0dev0", "2026-07-02", "new"))
assert doc.count("[v0.4.0dev0]") == 1
assert "new (#1)" in doc and "old (#1)" not in doc
# --- render_draft_notes ------------------------------------------------------
_REPO = "omnigent-ai/omnigent"
def test_draft_notes_groups_into_sections_by_type() -> None:
results = [
_result(10, "a new capability", ["Feature"]),
_result(20, "moved a button", ["UI / frontend change"]),
_result(30, "a crash fix", ["Bug fix"]),
_result(40, "dropped a flag", ["Breaking change"]),
]
notes = gen.render_draft_notes(results, _REPO)
assert "## Major new features" in notes
assert "## Breaking changes" in notes
assert "## Bug fixes" in notes
# Feature/UI land in features; Breaking and Bug fix each get their own section.
feat, rest = notes.split("## Breaking changes")
breaking, fixes = rest.split("## Bug fixes")
assert "a new capability (#10)" in feat and "moved a button (#20)" in feat
assert "dropped a flag (#40)" in breaking
assert "a crash fix (#30)" in fixes
# Sections appear in features → breaking → bug fixes order.
assert (
notes.index("## Major new features")
< notes.index("## Breaking changes")
< notes.index("## Bug fixes")
)
def test_draft_notes_has_full_changelog_footer() -> None:
notes = gen.render_draft_notes([_result(1, "x", ["Feature"])], _REPO)
assert notes.rstrip().endswith(
"Full Changelog: https://github.com/omnigent-ai/omnigent/blob/main/CHANGELOG.md"
)
def test_draft_notes_empty_section_keeps_placeholder() -> None:
# Only a feature entry — the bug-fixes section should still appear with a hint.
notes = gen.render_draft_notes([_result(1, "x", ["Feature"])], _REPO)
assert "## Bug fixes" in notes
assert "no entries harvested" in notes
def test_draft_notes_sorted_by_pr_within_section() -> None:
results = [
_result(30, "third", ["Feature"]),
_result(10, "first", ["Feature"]),
_result(20, "second", ["UI / frontend change"]),
]
notes = gen.render_draft_notes(results, _REPO)
assert notes.index("first (#10)") < notes.index("second (#20)") < notes.index("third (#30)")
# --- render_pr_list (agent input) --------------------------------------------
def _titled(pr: int, title: str, description: str, type_tags: list[str] | None = None) -> object:
r = gen.HarvestResult(pr, title)
r.description = description
r.type_tags = type_tags or []
r.status = "included" if description else "omitted"
return r
def test_pr_list_includes_title_and_tagged_description() -> None:
results = [
_titled(20, "feat(web): projects workspace", "group sessions", ["UI / frontend change"]),
_titled(10, "chore: bump deps", ""), # no changelog description — title only
]
listing = gen.render_pr_list(results)
# Sorted by PR number.
assert listing.index("#10:") < listing.index("#20:")
assert "#10: chore: bump deps" in listing
assert "#20: feat(web): projects workspace" in listing
assert " - [UI] group sessions" in listing
def test_pr_list_handles_missing_title() -> None:
listing = gen.render_pr_list([_titled(5, "", "")])
assert "#5: (no title)" in listing
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
_SCRIPT = Path(__file__).parents[2] / ".github" / "scripts" / "pr-template" / "format_body.py"
sys.path.insert(0, str(_SCRIPT.parent))
_SPEC = importlib.util.spec_from_file_location("pr_autoformat", _SCRIPT)
assert _SPEC is not None and _SPEC.loader is not None
pr_autoformat = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(pr_autoformat)
def test_wraps_existing_description_in_summary_without_deleting_it() -> None:
formatted = pr_autoformat.format_body("Fix the important thing.")
assert formatted.startswith("## Summary\n\nFix the important thing.")
assert "## Type of change" in formatted
assert "- [ ] Bug fix" in formatted
assert "## Test coverage" in formatted
assert "- [ ] Unit tests added / updated" in formatted
def test_preserves_existing_sections_and_adds_missing_optional_context() -> None:
original = "## Summary\n\nExisting summary.\n\n## Type of change\n\n- [x] Feature\n"
formatted = pr_autoformat.format_body(original)
assert "Existing summary." in formatted
assert "- [x] Feature" in formatted
assert formatted.count("## Summary") == 1
assert formatted.count("## Type of change") == 1
assert "## ELI5" in formatted
assert "## Diagram" in formatted
assert "## Test Plan" in formatted
assert "## Coverage notes" in formatted
assert "## Changelog" in formatted
def test_scaffolds_changelog_section_with_delete_placeholder() -> None:
formatted = pr_autoformat.format_body("Fix the important thing.")
assert "## Changelog" in formatted
# The scaffolded section defaults to the delete-if-not-noteworthy placeholder.
assert formatted.rstrip().endswith("else delete this section>")
+66
View File
@@ -0,0 +1,66 @@
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import pytest
SCRIPT = (
Path(__file__).resolve().parents[2] / ".github" / "scripts" / "pr-size" / "compute_label.py"
)
spec = importlib.util.spec_from_file_location("compute_pr_size", SCRIPT)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
@pytest.mark.parametrize(
"total, expected",
[
(0, "size/XS"),
(9, "size/XS"),
(10, "size/S"),
(49, "size/S"),
(50, "size/M"),
(199, "size/M"),
(200, "size/L"),
(499, "size/L"),
(500, "size/XL"),
(10_000, "size/XL"),
],
)
def test_size_label_boundaries(total: int, expected: str) -> None:
assert module.size_label(total) == expected
@pytest.mark.parametrize(
"filename",
["uv.lock", "package-lock.json", "web/package-lock.json", "web/electron/yarn.lock"],
)
def test_lock_files_are_generated(filename: str) -> None:
assert module.is_generated(filename)
@pytest.mark.parametrize(
"filename",
["omnigent/runtime/workflow.py", "docs/uv.lock.md", "src/package-lock.json.bak"],
)
def test_source_files_are_not_generated(filename: str) -> None:
assert not module.is_generated(filename)
def test_total_changes_excludes_generated() -> None:
files = [
{"filename": "omnigent/a.py", "additions": 30, "deletions": 10},
{"filename": "uv.lock", "additions": 5000, "deletions": 4000},
{"filename": "web/package-lock.json", "additions": 800, "deletions": 0},
]
# Only the source file counts: 30 + 10 = 40 -> size/S.
assert module.total_changes(files) == 40
assert module.size_label(module.total_changes(files)) == "size/S"
def test_total_changes_missing_fields_default_to_zero() -> None:
assert module.total_changes([{"filename": "x.py"}]) == 0
+300
View File
@@ -0,0 +1,300 @@
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
SCRIPT = (
Path(__file__).resolve().parents[2] / ".github" / "scripts" / "pr-template" / "validate.py"
)
spec = importlib.util.spec_from_file_location("validate_pr_template", SCRIPT)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
def _valid_body(
*,
summary: str = "- Improves the agent handoff flow and fixes stale polling.",
test_plan: str = (
"Added focused unit coverage for the cursor math and an E2E regression "
"that exercises the REPL path."
),
type_checkboxes: str = """
- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
""",
test_checkboxes: str = """
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [x] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
""",
coverage_notes: str | None = None,
demo: str | None = None,
changelog: str | None = "Stale polling no longer blocks the REPL handoff",
) -> str:
notes_section = "" if coverage_notes is None else f"\n## Coverage notes\n\n{coverage_notes}\n"
demo_section = "" if demo is None else f"\n## Demo\n\n{demo}\n"
changelog_section = "" if changelog is None else f"\n## Changelog\n\n{changelog}\n"
return f"""
## Summary
{summary}
## Test Plan
{test_plan}
{demo_section}
## Type of change
{type_checkboxes}
## Test coverage
{test_checkboxes}{notes_section}{changelog_section}"""
_BREAKING_TYPE = """
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [x] Breaking change
"""
_UI_CHANGE = """
- [ ] Bug fix
- [ ] Feature
- [x] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
"""
_MANUAL_ONLY = """
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
"""
_NOT_APPLICABLE_ONLY = """
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [x] Not applicable
"""
def test_valid_body() -> None:
result = module.validate_pr_body(_valid_body())
assert result.ok, result.errors
def test_validate_pr_body_accepts_leading_bom() -> None:
result = module.validate_pr_body("" + _valid_body())
assert result.ok, result.errors
def test_requires_type_and_test_checkboxes() -> None:
body = _valid_body(
type_checkboxes="""
- [ ] Bug fix
- [ ] Feature
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
""",
test_checkboxes="""
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
""",
)
result = module.validate_pr_body(body)
assert not result.ok
assert "Check at least one Type of change checkbox." in result.errors
assert "Check at least one Test coverage checkbox." in result.errors
def test_rejects_missing_template_labels() -> None:
body = _valid_body(
type_checkboxes="""
- [x] Bug fix
""",
test_checkboxes="""
- [x] Unit tests added / updated
""",
)
result = module.validate_pr_body(body)
assert not result.ok
assert any(error.startswith("Type of change is missing") for error in result.errors)
assert any(error.startswith("Test coverage is missing") for error in result.errors)
def test_rejects_missing_required_heading() -> None:
body = _valid_body().replace("## Test Plan", "## Test notes")
result = module.validate_pr_body(body)
assert not result.ok
assert "Missing required section: ## Test Plan" in result.errors
assert "Test Plan must describe how the change was tested." in result.errors
def test_rejects_placeholder_summary_and_test_plan() -> None:
body = _valid_body(
summary="<!-- Replace this with what changed and why. -->\nWhat changed and why?",
test_plan="How was this change tested?",
)
result = module.validate_pr_body(body)
assert not result.ok
assert "Summary still contains template placeholder text." in result.errors
assert "Test Plan still contains template placeholder text." in result.errors
def test_rejects_empty_summary_and_test_plan_after_html_comments() -> None:
body = _valid_body(
summary="<!-- Summary will be ignored because it is an HTML comment. -->",
test_plan="<!-- Test Plan will be ignored because it is an HTML comment. -->",
)
result = module.validate_pr_body(body)
assert not result.ok
assert "Summary must describe what changed and why." in result.errors
assert "Test Plan must describe how the change was tested." in result.errors
def test_coverage_notes_optional_for_automated_coverage() -> None:
# Default body checks Unit + E2E and omits the Coverage notes section.
result = module.validate_pr_body(_valid_body())
assert result.ok, result.errors
def test_manual_verification_requires_coverage_notes() -> None:
body = _valid_body(test_checkboxes=_MANUAL_ONLY)
result = module.validate_pr_body(body)
assert not result.ok
assert any(error.startswith("Coverage notes are required") for error in result.errors)
def test_not_applicable_requires_coverage_notes() -> None:
body = _valid_body(test_checkboxes=_NOT_APPLICABLE_ONLY)
result = module.validate_pr_body(body)
assert not result.ok
assert any(error.startswith("Coverage notes are required") for error in result.errors)
def test_manual_verification_with_coverage_notes_passes() -> None:
body = _valid_body(
test_checkboxes=_MANUAL_ONLY,
coverage_notes=(
"Verified manually by running the REPL handoff flow and confirming "
"polling resumes after a reconnect."
),
)
result = module.validate_pr_body(body)
assert result.ok, result.errors
def test_not_applicable_with_empty_coverage_notes_after_comment_is_rejected() -> None:
body = _valid_body(
test_checkboxes=_NOT_APPLICABLE_ONLY,
coverage_notes="<!-- nothing meaningful here -->",
)
result = module.validate_pr_body(body)
assert not result.ok
assert any(error.startswith("Coverage notes are required") for error in result.errors)
def test_demo_optional_for_non_ui_change() -> None:
# Default body is a bug fix with no Demo section.
result = module.validate_pr_body(_valid_body())
assert result.ok, result.errors
def test_ui_change_requires_demo() -> None:
body = _valid_body(type_checkboxes=_UI_CHANGE)
result = module.validate_pr_body(body)
assert not result.ok
assert any(error.startswith("Demo is required") for error in result.errors)
def test_ui_change_with_empty_demo_after_comment_is_rejected() -> None:
body = _valid_body(
type_checkboxes=_UI_CHANGE,
demo="<!-- nothing meaningful here -->",
)
result = module.validate_pr_body(body)
assert not result.ok
assert any(error.startswith("Demo is required") for error in result.errors)
def test_ui_change_with_demo_passes() -> None:
body = _valid_body(
type_checkboxes=_UI_CHANGE,
demo="![new settings panel](https://github.com/org/repo/assets/1/screenshot.png)",
)
result = module.validate_pr_body(body)
assert result.ok, result.errors
def test_changelog_free_text_line_is_valid() -> None:
body = _valid_body(changelog="`--watch` reruns an agent when files change")
result = module.validate_pr_body(body)
assert result.ok, result.errors
def test_changelog_section_is_optional() -> None:
# A non-breaking PR may omit the Changelog section entirely.
result = module.validate_pr_body(_valid_body(changelog=None))
assert result.ok, result.errors
def test_changelog_placeholder_left_in_is_allowed_for_non_breaking() -> None:
# Leaving the untouched placeholder is treated like deleting the section.
body = _valid_body(changelog="<Add a line to describe the change, else delete this section>")
result = module.validate_pr_body(body)
assert result.ok, result.errors
def test_breaking_change_requires_a_description() -> None:
body = _valid_body(type_checkboxes=_BREAKING_TYPE, changelog=None)
result = module.validate_pr_body(body)
assert not result.ok
assert any(error.startswith("A Breaking change must describe") for error in result.errors)
def test_breaking_change_with_placeholder_is_rejected() -> None:
body = _valid_body(
type_checkboxes=_BREAKING_TYPE,
changelog="<Add a line to describe the change, else delete this section>",
)
result = module.validate_pr_body(body)
assert not result.ok
assert any(error.startswith("A Breaking change must describe") for error in result.errors)
def test_breaking_change_with_description_passes() -> None:
body = _valid_body(
type_checkboxes=_BREAKING_TYPE,
changelog="Dropped the deprecated `--legacy` flag",
)
result = module.validate_pr_body(body)
assert result.ok, result.errors
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
SCRIPT = (
Path(__file__).resolve().parents[2] / ".github" / "scripts" / "changelog" / "release_to_mdx.py"
)
spec = importlib.util.spec_from_file_location("release_to_mdx", SCRIPT)
assert spec and spec.loader
mod = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = mod
spec.loader.exec_module(mod)
def test_mdx_escape_braces_and_angle_brackets() -> None:
out = mod.mdx_escape("use {config} and <Component> safely")
assert "{" not in out and "}" not in out
assert "<" not in out # escaped to &lt;
assert "&#123;config&#125;" in out
def test_mdx_escape_unwraps_autolinks() -> None:
out = mod.mdx_escape("see <https://example.com/x> now")
assert "https://example.com/x" in out
assert "<https" not in out and "&lt;https" not in out
def test_mdx_escape_preserves_blockquote_gt() -> None:
# '>' is left alone so Markdown blockquotes still render.
assert ">" in mod.mdx_escape("> a quote")
def test_linkify_pr_refs() -> None:
out = mod.linkify_pr_refs("fixed in #1304 and #20", "omnigent-ai/omnigent")
assert "[#1304](https://github.com/omnigent-ai/omnigent/pull/1304)" in out
assert "[#20](https://github.com/omnigent-ai/omnigent/pull/20)" in out
def test_linkify_leaves_headings_alone() -> None:
# Heading "# v0.3.0" has a space after '#', so it is not a PR ref.
assert mod.linkify_pr_refs("# v0.3.0", "o/o") == "# v0.3.0"
def test_release_body_to_mdx_structure() -> None:
body = "### Major new features\n\n* Seven harnesses (#1132, #330)\n"
page = mod.release_body_to_mdx("v0.3.0", "2026-06-27", body, "omnigent-ai/omnigent")
assert page.startswith("{/* Auto-generated")
assert "# v0.3.0" in page
assert "_Released 2026-06-27_" in page
assert "### Major new features" in page
assert "/pull/1132" in page # PR refs linkified