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="\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="", test_plan="", ) 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="", ) 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="", ) 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="") 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="", ) 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