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
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:
@@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user