fix: #412/#413/#414 runtime stability and PR governance preflight
## #413 — runtime_verify_monitor.sh pipefail fix - find_live_pids() now captures pgrep output via local variable with || true so pipefail never triggers on no-match (pgrep exit 1) - Regression test added: monitor survives MAX_LOOPS=1 without crash ## #412 — startup crash logging improvement - Add asyncio.CancelledError catch in sync_positions_from_broker call so BaseException-level cancellations are logged before propagating - Provides evidence in run log if CancelledError causes future startup aborts ## #414 — PR governance preflight - validate_pr_body.py: add REQ-ID/TASK-ID/TEST-ID pattern checks (--no-governance flag to skip) - docs/workflow.md: new "PR Governance Preflight (Mandatory)" section - docs/commands.md: "PR Body Governance Preflight" section before tea pulls create - Tests: 4 new governance traceability tests in test_validate_pr_body.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -158,3 +158,46 @@ def test_run_overnight_fails_when_process_exits_before_grace_period(tmp_path: Pa
|
||||
watchdog_pid = int(watchdog_pid_file.read_text(encoding="utf-8").strip())
|
||||
with pytest.raises(ProcessLookupError):
|
||||
os.kill(watchdog_pid, 0)
|
||||
|
||||
|
||||
def test_runtime_verify_monitor_survives_when_no_live_pid(tmp_path: Path) -> None:
|
||||
"""Regression test for #413: monitor loop must not exit when pgrep finds no live process.
|
||||
|
||||
With set -euo pipefail, pgrep returning exit 1 (no match) would cause the
|
||||
whole script to abort via the pipefail mechanism. The fix captures pgrep
|
||||
output via a local variable with || true so pipefail is never triggered.
|
||||
|
||||
Verifies that the script: (1) exits 0 after completing MAX_LOOPS=1, and
|
||||
(2) logs a HEARTBEAT entry. Whether live_pids is 'none' or not depends on
|
||||
what processes happen to be running; either way the script must not crash.
|
||||
"""
|
||||
log_dir = tmp_path / "overnight"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"ROOT_DIR": str(REPO_ROOT),
|
||||
"LOG_DIR": str(log_dir),
|
||||
"INTERVAL_SEC": "1",
|
||||
"MAX_HOURS": "1",
|
||||
"MAX_LOOPS": "1",
|
||||
"POLICY_TZ": "UTC",
|
||||
}
|
||||
)
|
||||
completed = subprocess.run(
|
||||
["bash", str(RUNTIME_MONITOR)],
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 0, (
|
||||
f"monitor exited non-zero (pipefail regression?): {completed.stderr}"
|
||||
)
|
||||
log_text = _latest_runtime_log(log_dir)
|
||||
assert "[INFO] runtime verify monitor started" in log_text
|
||||
assert "[HEARTBEAT]" in log_text, "monitor did not complete a heartbeat cycle"
|
||||
# live_pids may be 'none' (no match) or a pid (process found) — either is valid.
|
||||
# The critical invariant is that the script survived the loop without pipefail abort.
|
||||
|
||||
@@ -37,6 +37,7 @@ def test_validate_pr_body_text_allows_escaped_newline_in_code_blocks() -> None:
|
||||
[
|
||||
"## Summary",
|
||||
"- example uses `\\n` for explanation",
|
||||
"- REQ-OPS-001 / TASK-OPS-001 / TEST-OPS-001",
|
||||
"```bash",
|
||||
"printf 'line1\\nline2\\n'",
|
||||
"```",
|
||||
@@ -63,7 +64,7 @@ def test_validate_pr_body_text_passes_with_valid_markdown() -> None:
|
||||
text = "\n".join(
|
||||
[
|
||||
"## Summary",
|
||||
"- item",
|
||||
"- REQ-OPS-001 / TASK-OPS-001 / TEST-OPS-001",
|
||||
"",
|
||||
"## Validation",
|
||||
"```bash",
|
||||
@@ -74,6 +75,34 @@ def test_validate_pr_body_text_passes_with_valid_markdown() -> None:
|
||||
assert module.validate_pr_body_text(text) == []
|
||||
|
||||
|
||||
def test_validate_pr_body_text_detects_missing_req_id() -> None:
|
||||
module = _load_module()
|
||||
text = "## Summary\n- TASK-OPS-001 / TEST-OPS-001 item\n"
|
||||
errors = module.validate_pr_body_text(text)
|
||||
assert any("REQ-ID" in err for err in errors)
|
||||
|
||||
|
||||
def test_validate_pr_body_text_detects_missing_task_id() -> None:
|
||||
module = _load_module()
|
||||
text = "## Summary\n- REQ-OPS-001 / TEST-OPS-001 item\n"
|
||||
errors = module.validate_pr_body_text(text)
|
||||
assert any("TASK-ID" in err for err in errors)
|
||||
|
||||
|
||||
def test_validate_pr_body_text_detects_missing_test_id() -> None:
|
||||
module = _load_module()
|
||||
text = "## Summary\n- REQ-OPS-001 / TASK-OPS-001 item\n"
|
||||
errors = module.validate_pr_body_text(text)
|
||||
assert any("TEST-ID" in err for err in errors)
|
||||
|
||||
|
||||
def test_validate_pr_body_text_skips_governance_when_disabled() -> None:
|
||||
module = _load_module()
|
||||
text = "## Summary\n- item without any IDs\n"
|
||||
errors = module.validate_pr_body_text(text, check_governance=False)
|
||||
assert not any("REQ-ID" in err or "TASK-ID" in err or "TEST-ID" in err for err in errors)
|
||||
|
||||
|
||||
def test_fetch_pr_body_reads_body_from_tea_api(monkeypatch) -> None:
|
||||
module = _load_module()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user