fix(sop-checklist): skip blank lines when scanning for section content
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 6s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 15s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 20s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 19s
review-check-tests / review-check.sh regression tests (pull_request) Successful in 21s
CI / Detect changes (pull_request) Successful in 30s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 7s
gate-check-v3 / gate-check (pull_request) Failing after 33s
qa-review / approved (pull_request) Failing after 21s
sop-tier-check / tier-check (pull_request) Successful in 19s
security-review / approved (pull_request) Failing after 23s
E2E API Smoke Test / detect-changes (pull_request) Successful in 52s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 52s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 52s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 7s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 7s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m24s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Failing after 1m26s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
CI / Platform (Go) (pull_request) Successful in 7m10s
CI / Python Lint & Test (pull_request) Successful in 7m13s
CI / all-required (pull_request) Failing after 20m11s
CI / Canvas (Next.js) (pull_request) Failing after 20m11s
CI / Canvas Deploy Reminder (pull_request) Has been cancelled

section_marker_present() checked only the immediately next line when the
marker line had no trailing content.  Markdown-header PR bodies use the
pattern:

    ## Comprehensive testing performed

    - go test -v ...

where a blank line separates the header from the content.  The function
saw the blank line (empty after stripping), returned False, and reported
"body-unfilled" for every section — causing "acked: 0/7 — body-unfilled"
on PRs whose bodies were correctly filled.

Fix: loop forward, skipping sequences of \n characters, until we find a
line with non-whitespace content or reach end-of-body.  This also
handles the edge case where the PR author leaves only blank lines after
the marker (still correctly rejected).

Add tests for:
- marker with one blank line before content (the actual PR pattern)
- marker with multiple blank lines before content
- marker with only blank lines after header (must still be False)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 02:40:39 +00:00
parent da79f17096
commit 05ef0964f6
2 changed files with 54 additions and 13 deletions
+27 -9
View File
@@ -188,8 +188,8 @@ def section_marker_present(body: str, marker: str) -> bool:
on a non-empty line (i.e. the author actually filled it in).
We require the marker substring AND non-whitespace content on the
same line OR within the next line — this prevents trivially-empty
checklists like:
same line OR within the next non-blank line — this prevents
trivially-empty checklists like:
## SOP-Checklist
- [ ] **Comprehensive testing performed**:
@@ -198,6 +198,10 @@ def section_marker_present(body: str, marker: str) -> bool:
from auto-passing the section-present check. The peer-ack is still
required, but answering with empty content is captured as a soft
finding via the section-present test alone.
NOTE: we scan forward through blank lines (the markdown-header pattern
is ## Header\\n\\ncontent) so that a header + blank-line + content
structure still satisfies the check.
"""
if not body or not marker:
return False
@@ -216,13 +220,27 @@ def section_marker_present(body: str, marker: str) -> bool:
stripped = re.sub(r"[\s\*:\-\[\]]+", "", line)
if stripped:
return True
# Fall through: check the NEXT line (multi-line answers).
next_line_end = body.find("\n", line_end + 1)
if next_line_end < 0:
next_line_end = len(body)
next_line = body[line_end + 1:next_line_end]
stripped_next = re.sub(r"[\s\*:\-\[\]]+", "", next_line)
return bool(stripped_next)
# Fall through: scan forward, skipping blank-only lines, until we find
# non-empty content or run out of body. Handles:
# ## Header ← marker line (empty after marker)
# ← blank line (skipped)
# - actual content ← found
pos = line_end
while True:
# Skip the current newline and any additional newlines (blank lines).
while pos < len(body) and body[pos] == "\n":
pos += 1
if pos >= len(body):
break
line_end = body.find("\n", pos)
if line_end < 0:
line_end = len(body)
line = body[pos:line_end]
stripped = re.sub(r"[\s\*:\-\[\]]+", "", line)
if stripped:
return True
pos = line_end
return False
# ---------------------------------------------------------------------------
+27 -4
View File
@@ -135,8 +135,8 @@ class TestParseDirectives(unittest.TestCase):
self.aliases = _numeric_aliases()
def parse_ack_revoke(self, body):
directives, na_directives = sop.parse_directives(body, self.aliases)
self.assertEqual(na_directives, [])
# parse_directives now returns list[tuple] (na_directives dropped per PR #1263).
directives = sop.parse_directives(body, self.aliases)
return directives
def test_simple_ack(self):
@@ -201,8 +201,8 @@ class TestParseDirectives(unittest.TestCase):
self.assertEqual(len(d), 1)
def test_empty_body(self):
self.assertEqual(sop.parse_directives("", self.aliases), ([], []))
self.assertEqual(sop.parse_directives(None, self.aliases), ([], []))
self.assertEqual(sop.parse_directives("", self.aliases), [])
self.assertEqual(sop.parse_directives(None, self.aliases), [])
def test_normalization_applied(self):
# /sop-ack Comprehensive_Testing → canonical comprehensive-testing
@@ -243,6 +243,29 @@ class TestSectionMarkerPresent(unittest.TestCase):
body = "- [ ] **comprehensive TESTING performed**: yes"
self.assertTrue(sop.section_marker_present(body, "Comprehensive testing performed"))
def test_marker_with_blank_line_before_content(self):
# Markdown-header pattern: ## Header\n\n- content.
# The blank line between header and content must NOT cause a false-negative.
body = (
"## Comprehensive testing performed\n\n"
"- `go test -v ./workspace-server/internal/secrets/...` — all 8 tests pass.\n"
"- `go test -cover ...` — 100.0% coverage.\n"
)
self.assertTrue(sop.section_marker_present(body, "Comprehensive testing performed"))
def test_marker_with_multiple_blank_lines_before_content(self):
# Three blank lines before content still passes.
body = (
"## Staging-smoke verified or pending\n\n\n\n"
"Post-merge: go test ./internal/secrets/... on the merged staging branch.\n"
)
self.assertTrue(sop.section_marker_present(body, "Staging-smoke verified or pending"))
def test_marker_with_only_blank_lines_after_header(self):
# Marker present but only blank lines follow → no real content → False.
body = "## Root-cause not symptom\n\n \n\n"
self.assertFalse(sop.section_marker_present(body, "Root-cause not symptom"))
def test_empty_body(self):
self.assertFalse(sop.section_marker_present("", "X"))
self.assertFalse(sop.section_marker_present(None, "X"))