Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f95f9912f8 |
@@ -1,6 +1,6 @@
|
||||
<!--
|
||||
For AI-written descriptions:
|
||||
- Follow this template (Related issue, Summary, Type of change, Test coverage, Coverage rationale).
|
||||
- Follow this template (Related issue, Summary, Test Plan, Type of change, Test coverage, Coverage notes).
|
||||
- Keep it concise; reviewers skim long descriptions.
|
||||
- For non-trivial changes, include an ELI5 and a diagram (ASCII or mermaid).
|
||||
- Leave every checkbox in place. The PR Template check fails if required sections
|
||||
@@ -23,6 +23,10 @@ Closes #
|
||||
|
||||
<!-- What changed and why, in 1-3 bullets or a short paragraph. -->
|
||||
|
||||
## Test Plan
|
||||
|
||||
<!-- How was this change tested? Describe the steps, commands, or scenarios used to verify it. Include a screenshot or recording where helpful. -->
|
||||
|
||||
## Type of change
|
||||
|
||||
- [ ] Bug fix
|
||||
@@ -43,11 +47,10 @@ Closes #
|
||||
- [ ] Existing tests cover this change
|
||||
- [ ] Not applicable
|
||||
|
||||
## Coverage rationale
|
||||
## Coverage notes
|
||||
|
||||
<!--
|
||||
Describe the exact commands run and the coverage added/updated. If you did not
|
||||
add or run tests, explain why the existing coverage is enough or why tests are
|
||||
not applicable. For E2E-relevant changes, call out the E2E scenario exercised or
|
||||
why no E2E coverage was added.
|
||||
Optional — but required if you checked "Manual verification completed" or
|
||||
"Not applicable" above. Describe what you verified manually, or why automated
|
||||
test coverage is not needed for this change.
|
||||
-->
|
||||
|
||||
@@ -40,6 +40,12 @@ def format_body(body: str) -> str:
|
||||
elif not _has_heading(body, "Summary"):
|
||||
body = f"## Summary\n\n{body}"
|
||||
|
||||
body = _append_section(
|
||||
body,
|
||||
"Test Plan",
|
||||
"How was this change tested? Describe the steps, commands, or scenarios "
|
||||
"used to verify it (autoformat added this section — please replace it).",
|
||||
)
|
||||
body = _append_section(
|
||||
body,
|
||||
"ELI5",
|
||||
@@ -54,9 +60,9 @@ def format_body(body: str) -> str:
|
||||
body = _append_section(body, "Test coverage", _checkbox_block(TEST_LABELS))
|
||||
body = _append_section(
|
||||
body,
|
||||
"Coverage rationale",
|
||||
"Autoformat added this section; please add commands run or explain why "
|
||||
"coverage is sufficient.",
|
||||
"Coverage notes",
|
||||
"<!-- Optional; required if you checked 'Manual verification completed' "
|
||||
"or 'Not applicable' above. -->",
|
||||
)
|
||||
return body.rstrip() + "\n"
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ import sys
|
||||
|
||||
REQUIRED_HEADINGS = (
|
||||
"Summary",
|
||||
"Test Plan",
|
||||
"Type of change",
|
||||
"Test coverage",
|
||||
"Coverage rationale",
|
||||
)
|
||||
|
||||
TYPE_LABELS = (
|
||||
@@ -40,10 +40,8 @@ TEST_LABELS = (
|
||||
PLACEHOLDER_FRAGMENTS = (
|
||||
"what changed and why",
|
||||
"check all that apply",
|
||||
"describe the exact commands",
|
||||
"describe below",
|
||||
"explain why",
|
||||
"if you did not add or run tests",
|
||||
"how was this change tested",
|
||||
)
|
||||
|
||||
|
||||
@@ -121,6 +119,12 @@ def validate_pr_body(body: str) -> ValidationResult:
|
||||
elif _contains_placeholder(summary):
|
||||
errors.append("Summary still contains template placeholder text.")
|
||||
|
||||
test_plan = _meaningful_text(_section(body, spans, "Test Plan"))
|
||||
if not test_plan:
|
||||
errors.append("Test Plan must describe how the change was tested.")
|
||||
elif _contains_placeholder(test_plan):
|
||||
errors.append("Test Plan still contains template placeholder text.")
|
||||
|
||||
type_section = _section(body, spans, "Type of change")
|
||||
missing_type_labels = _missing_labels(type_section, TYPE_LABELS)
|
||||
if missing_type_labels:
|
||||
@@ -141,31 +145,19 @@ def validate_pr_body(body: str) -> ValidationResult:
|
||||
if not checked_tests:
|
||||
errors.append("Check at least one Test coverage checkbox.")
|
||||
|
||||
rationale = _meaningful_text(_section(body, spans, "Coverage rationale"))
|
||||
if not rationale:
|
||||
errors.append(
|
||||
"Coverage rationale must explain tests run/added, or why more coverage is not needed."
|
||||
)
|
||||
elif _contains_placeholder(rationale):
|
||||
errors.append("Coverage rationale still contains template placeholder text.")
|
||||
|
||||
automated_tests = {
|
||||
"Unit tests added / updated",
|
||||
"Integration tests added / updated",
|
||||
"E2E tests added / updated",
|
||||
"Existing tests cover this change",
|
||||
}
|
||||
if checked_tests and checked_tests.isdisjoint(automated_tests):
|
||||
if len(rationale.split()) < 8:
|
||||
# Coverage notes are optional in general, but required whenever "Manual
|
||||
# verification completed" or "Not applicable" is checked — those choices
|
||||
# need a written justification.
|
||||
if checked_tests & {"Manual verification completed", "Not applicable"}:
|
||||
coverage_notes = _meaningful_text(_section(body, spans, "Coverage notes"))
|
||||
if not coverage_notes:
|
||||
errors.append(
|
||||
"When no automated test coverage checkbox is selected, "
|
||||
"the rationale must explain why."
|
||||
"Coverage notes are required when 'Manual verification completed' or "
|
||||
"'Not applicable' is selected — describe what you verified or why "
|
||||
"automated coverage is not needed."
|
||||
)
|
||||
|
||||
if "Not applicable" in checked_tests and rationale and len(rationale.split()) < 8:
|
||||
errors.append(
|
||||
"Not applicable test coverage requires a concrete explanation in Coverage rationale."
|
||||
)
|
||||
elif _contains_placeholder(coverage_notes):
|
||||
errors.append("Coverage notes still contains template placeholder text.")
|
||||
|
||||
return ValidationResult(ok=not errors, errors=errors)
|
||||
|
||||
|
||||
@@ -32,4 +32,5 @@ def test_preserves_existing_sections_and_adds_missing_optional_context() -> None
|
||||
assert formatted.count("## Type of change") == 1
|
||||
assert "## ELI5" in formatted
|
||||
assert "## Diagram" in formatted
|
||||
assert "## Coverage rationale" in formatted
|
||||
assert "## Test Plan" in formatted
|
||||
assert "## Coverage notes" in formatted
|
||||
|
||||
@@ -17,6 +17,10 @@ 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
|
||||
@@ -33,33 +37,50 @@ def _valid_body(
|
||||
- [ ] Existing tests cover this change
|
||||
- [ ] Not applicable
|
||||
""",
|
||||
rationale: str = (
|
||||
"Added focused unit coverage for the cursor math and an E2E regression "
|
||||
"that exercises the REPL path."
|
||||
),
|
||||
coverage_notes: str | None = None,
|
||||
) -> str:
|
||||
notes_section = "" if coverage_notes is None else f"\n## Coverage notes\n\n{coverage_notes}\n"
|
||||
return f"""
|
||||
## Summary
|
||||
|
||||
{summary}
|
||||
|
||||
## Test Plan
|
||||
|
||||
{test_plan}
|
||||
|
||||
## Type of change
|
||||
{type_checkboxes}
|
||||
## Test coverage
|
||||
{test_checkboxes}
|
||||
## Coverage rationale
|
||||
{test_checkboxes}{notes_section}"""
|
||||
|
||||
{rationale}
|
||||
|
||||
_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_with_e2e_rationale() -> None:
|
||||
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("\ufeff" + _valid_body())
|
||||
result = module.validate_pr_body("" + _valid_body())
|
||||
assert result.ok, result.errors
|
||||
|
||||
|
||||
@@ -81,7 +102,6 @@ def test_requires_type_and_test_checkboxes() -> None:
|
||||
- [ ] Existing tests cover this change
|
||||
- [ ] Not applicable
|
||||
""",
|
||||
rationale="No tests because this is a documentation-only link update.",
|
||||
)
|
||||
result = module.validate_pr_body(body)
|
||||
assert not result.ok
|
||||
@@ -89,34 +109,6 @@ def test_requires_type_and_test_checkboxes() -> None:
|
||||
assert "Check at least one Test coverage checkbox." in result.errors
|
||||
|
||||
|
||||
def test_requires_explanation_when_not_applicable() -> None:
|
||||
body = _valid_body(
|
||||
type_checkboxes="""
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature
|
||||
- [ ] Refactor / chore
|
||||
- [x] 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
|
||||
- [x] Not applicable
|
||||
""",
|
||||
rationale="Docs only.",
|
||||
)
|
||||
result = module.validate_pr_body(body)
|
||||
assert not result.ok
|
||||
assert (
|
||||
"Not applicable test coverage requires a concrete explanation in Coverage rationale."
|
||||
in result.errors
|
||||
)
|
||||
|
||||
|
||||
def test_rejects_missing_template_labels() -> None:
|
||||
body = _valid_body(
|
||||
type_checkboxes="""
|
||||
@@ -125,7 +117,6 @@ def test_rejects_missing_template_labels() -> None:
|
||||
test_checkboxes="""
|
||||
- [x] Unit tests added / updated
|
||||
""",
|
||||
rationale="Added tests for the changed parser behavior.",
|
||||
)
|
||||
result = module.validate_pr_body(body)
|
||||
assert not result.ok
|
||||
@@ -134,80 +125,72 @@ def test_rejects_missing_template_labels() -> None:
|
||||
|
||||
|
||||
def test_rejects_missing_required_heading() -> None:
|
||||
body = _valid_body().replace("## Coverage rationale", "## Test notes")
|
||||
body = _valid_body().replace("## Test Plan", "## Test notes")
|
||||
result = module.validate_pr_body(body)
|
||||
assert not result.ok
|
||||
assert "Missing required section: ## Coverage rationale" in result.errors
|
||||
assert (
|
||||
"Coverage rationale must explain tests run/added, or why more coverage is not needed."
|
||||
in result.errors
|
||||
)
|
||||
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_rationale() -> None:
|
||||
def test_rejects_placeholder_summary_and_test_plan() -> None:
|
||||
body = _valid_body(
|
||||
summary="<!-- Replace this with what changed and why. -->\nWhat changed and why?",
|
||||
rationale="Describe the exact commands you ran and coverage added.",
|
||||
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 "Coverage rationale 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_rationale_after_html_comments() -> None:
|
||||
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. -->",
|
||||
rationale="<!-- Rationale 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 (
|
||||
"Coverage rationale must explain tests run/added, or why more coverage is not needed."
|
||||
in result.errors
|
||||
)
|
||||
assert "Test Plan must describe how the change was tested." in result.errors
|
||||
|
||||
|
||||
def test_requires_explanation_when_only_manual_coverage_selected() -> None:
|
||||
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="""
|
||||
- [ ] Unit tests added / updated
|
||||
- [ ] Integration tests added / updated
|
||||
- [ ] E2E tests added / updated
|
||||
- [x] Manual verification completed
|
||||
- [ ] Existing tests cover this change
|
||||
- [ ] Not applicable
|
||||
""",
|
||||
rationale="Ran locally.",
|
||||
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 (
|
||||
"When no automated test coverage checkbox is selected, the rationale must explain why."
|
||||
in result.errors
|
||||
)
|
||||
|
||||
|
||||
def test_empty_not_applicable_rationale_does_not_duplicate_short_rationale_error() -> None:
|
||||
body = _valid_body(
|
||||
test_checkboxes="""
|
||||
- [ ] Unit tests added / updated
|
||||
- [ ] Integration tests added / updated
|
||||
- [ ] E2E tests added / updated
|
||||
- [ ] Manual verification completed
|
||||
- [ ] Existing tests cover this change
|
||||
- [x] Not applicable
|
||||
""",
|
||||
rationale="",
|
||||
)
|
||||
result = module.validate_pr_body(body)
|
||||
assert not result.ok
|
||||
assert (
|
||||
"Coverage rationale must explain tests run/added, or why more coverage is not needed."
|
||||
in result.errors
|
||||
)
|
||||
assert (
|
||||
"Not applicable test coverage requires a concrete explanation in Coverage rationale."
|
||||
not in result.errors
|
||||
)
|
||||
assert any(error.startswith("Coverage notes are required") for error in result.errors)
|
||||
|
||||
Reference in New Issue
Block a user