Phase A (safety): - scripts/p3_rollback.sh (+test): restore all privileged P3 surfaces from a recorded baseline; --dry-run default, --apply gated. Correct App-uninstall (App JWT) model; per-task env-reviewer restore by numeric id; real protection post-restore assert (normalize reads argv, fails loud, divergent state exits non-zero — regression-tested). KNOWN-LIMITATIONS header flags the branch-protection GET->PUT transform + live-validation for the C1 gate. - scripts/assert_no_write_token.py (+test): box/CI audit that no write token (incl. ghu_/ghr_ prefixes + App PEM) lives on the box. - draft_pr_monitor.py (+test): runaway (>3/15min) + stale (7d) draft-PR sweep, wired into tick() and bound a read-only provider in serve. Phase B (wiring): systemd EnvironmentFile P3 vars + verification; new-draft-PR lifecycle notice. Phase E (docs): P3-LIVE-FLIP-PLAN/README/ci-README reflect CI-live-since-6/22 + box-integration; runbook consolidated (rollback Incident 7 + box-env wiring); removed a stray duplicate runbook. Suite: 1360 passed, ruff clean. Branch only; not merged/deployed. REMAINING HUMAN GATES: C1 /sh-security-review + GPT-4.1 cross-review on the enabled workflow + rollback script; D box deploy + smoke + merge.
400 lines
12 KiB
Python
400 lines
12 KiB
Python
"""Unit tests for agent_team.draft_pr_monitor (P3 box-integration, A4).
|
|
|
|
Covers the draft-PR runaway/stale sweep: it ALARMs when > 3 draft PRs open
|
|
within 15 minutes, reminds on a draft PR idle > 7 days, applies flapping backoff
|
|
(one ALARM per cooldown, one reminder per PR per cooldown), and never auto-closes
|
|
or self-stops. No network: every side effect (alarm, reminder) is an injected
|
|
callable, exactly as the module's contract promises.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from agent_team.draft_pr_monitor import (
|
|
DEFAULT_ALARM_COOLDOWN,
|
|
DEFAULT_STALE_COOLDOWN,
|
|
DraftPr,
|
|
MonitorAction,
|
|
MonitorMemory,
|
|
run_draft_pr_monitor,
|
|
)
|
|
|
|
# A fixed "now"; ISO strings mirror the GitHub API / ledger (UTC).
|
|
_NOW = datetime(2026, 6, 23, 12, 0, 0, tzinfo=timezone.utc)
|
|
|
|
|
|
def _ago(**kwargs: float) -> str:
|
|
return (_NOW - timedelta(**kwargs)).isoformat()
|
|
|
|
|
|
class _Recorder:
|
|
"""Records the runaway counts / stale PRs handed to the side-effect hooks."""
|
|
|
|
def __init__(self) -> None:
|
|
self.alarms: list[int] = []
|
|
self.reminders: list[int] = []
|
|
|
|
def on_alarm(self, opened_in_window: int) -> None:
|
|
self.alarms.append(opened_in_window)
|
|
|
|
def on_stale_reminder(self, pr: DraftPr) -> None:
|
|
self.reminders.append(pr.number)
|
|
|
|
|
|
def _open_burst(count: int, *, within_minutes: int = 5) -> list[DraftPr]:
|
|
"""``count`` draft PRs all opened ``within_minutes`` ago (inside the window)."""
|
|
return [
|
|
DraftPr(
|
|
number=i,
|
|
opened_at=_ago(minutes=within_minutes),
|
|
updated_at=_NOW.isoformat(),
|
|
)
|
|
for i in range(count)
|
|
]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Runaway ALARM threshold (> 3 within 15 min)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_runaway_alarm_fires_above_threshold() -> None:
|
|
rec = _Recorder()
|
|
report = run_draft_pr_monitor(
|
|
_open_burst(4),
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
now=_NOW,
|
|
)
|
|
assert report.alarmed == 1
|
|
assert rec.alarms == [4]
|
|
assert report.outcomes[0].action is MonitorAction.ALARMED
|
|
|
|
|
|
def test_exactly_threshold_does_not_alarm() -> None:
|
|
"""The rule is STRICTLY greater than 3 — exactly 3 must NOT alarm."""
|
|
rec = _Recorder()
|
|
report = run_draft_pr_monitor(
|
|
_open_burst(3),
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
now=_NOW,
|
|
)
|
|
assert report.alarmed == 0
|
|
assert rec.alarms == []
|
|
|
|
|
|
def test_prs_opened_outside_window_are_not_counted() -> None:
|
|
"""5 PRs but only 2 inside the 15-min window -> no alarm."""
|
|
rec = _Recorder()
|
|
prs = [
|
|
DraftPr(number=1, opened_at=_ago(minutes=2)),
|
|
DraftPr(number=2, opened_at=_ago(minutes=10)),
|
|
DraftPr(number=3, opened_at=_ago(minutes=20)),
|
|
DraftPr(number=4, opened_at=_ago(minutes=40)),
|
|
DraftPr(number=5, opened_at=_ago(hours=3)),
|
|
]
|
|
report = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
now=_NOW,
|
|
)
|
|
assert report.opened_in_window == 2
|
|
assert report.alarmed == 0
|
|
assert rec.alarms == []
|
|
|
|
|
|
def test_unparseable_opened_at_is_skipped_for_runaway() -> None:
|
|
"""A PR with a junk/missing opened_at is not counted and does not crash."""
|
|
rec = _Recorder()
|
|
prs = _open_burst(4) + [DraftPr(number=99, opened_at="not-a-date")]
|
|
report = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
now=_NOW,
|
|
)
|
|
# The 4 valid PRs still breach; the junk one is just ignored for the count.
|
|
assert report.opened_in_window == 4
|
|
assert report.alarmed == 1
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Flapping backoff — ALARM at most once per cooldown
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_runaway_alarm_suppressed_within_cooldown() -> None:
|
|
rec = _Recorder()
|
|
mem = MonitorMemory()
|
|
prs = _open_burst(5)
|
|
|
|
first = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=_NOW,
|
|
)
|
|
assert first.alarmed == 1
|
|
|
|
# A second tick a minute later, condition still breaching -> suppressed.
|
|
second = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=_NOW + timedelta(minutes=1),
|
|
)
|
|
assert second.alarmed == 0
|
|
assert second.alarm_suppressed == 1
|
|
assert rec.alarms == [5] # only the first post
|
|
|
|
|
|
def test_runaway_alarm_refires_after_cooldown() -> None:
|
|
rec = _Recorder()
|
|
mem = MonitorMemory()
|
|
prs = _open_burst(5, within_minutes=1)
|
|
|
|
run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=_NOW,
|
|
)
|
|
# Past the cooldown, a still-breaching condition ALARMs again. Re-anchor the
|
|
# PRs so they remain inside the window relative to the later "now".
|
|
later = _NOW + DEFAULT_ALARM_COOLDOWN + timedelta(seconds=1)
|
|
prs_later = [
|
|
DraftPr(number=p.number, opened_at=(later - timedelta(minutes=1)).isoformat())
|
|
for p in prs
|
|
]
|
|
report = run_draft_pr_monitor(
|
|
prs_later,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=later,
|
|
)
|
|
assert report.alarmed == 1
|
|
assert len(rec.alarms) == 2
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Stale reminder (> 7 days idle), never auto-closes
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_stale_pr_reminds_once() -> None:
|
|
rec = _Recorder()
|
|
prs = [DraftPr(number=7, opened_at=_ago(days=30), updated_at=_ago(days=8))]
|
|
report = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
now=_NOW,
|
|
)
|
|
assert report.stale_reminded == 1
|
|
assert rec.reminders == [7]
|
|
|
|
|
|
def test_fresh_pr_does_not_remind() -> None:
|
|
rec = _Recorder()
|
|
prs = [DraftPr(number=7, opened_at=_ago(days=30), updated_at=_ago(days=6))]
|
|
report = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
now=_NOW,
|
|
)
|
|
assert report.stale_reminded == 0
|
|
assert rec.reminders == []
|
|
|
|
|
|
def test_exactly_7_days_does_not_remind() -> None:
|
|
"""The rule is strictly MORE than 7 days idle."""
|
|
rec = _Recorder()
|
|
prs = [DraftPr(number=7, updated_at=_ago(days=7))]
|
|
report = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
now=_NOW,
|
|
)
|
|
assert report.stale_reminded == 0
|
|
|
|
|
|
def test_stale_reminder_suppressed_within_cooldown() -> None:
|
|
rec = _Recorder()
|
|
mem = MonitorMemory()
|
|
prs = [DraftPr(number=7, updated_at=_ago(days=10))]
|
|
|
|
run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=_NOW,
|
|
)
|
|
second = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=_NOW + timedelta(hours=1),
|
|
)
|
|
assert second.stale_reminded == 0
|
|
assert second.stale_suppressed == 1
|
|
assert rec.reminders == [7] # only the first
|
|
|
|
|
|
def test_stale_reminder_refires_after_cooldown() -> None:
|
|
rec = _Recorder()
|
|
mem = MonitorMemory()
|
|
|
|
run_draft_pr_monitor(
|
|
[DraftPr(number=7, updated_at=_ago(days=10))],
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=_NOW,
|
|
)
|
|
later = _NOW + DEFAULT_STALE_COOLDOWN + timedelta(seconds=1)
|
|
report = run_draft_pr_monitor(
|
|
[DraftPr(number=7, updated_at=(later - timedelta(days=10)).isoformat())],
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=later,
|
|
)
|
|
assert report.stale_reminded == 1
|
|
assert len(rec.reminders) == 2
|
|
|
|
|
|
def test_unparseable_updated_at_is_skipped_for_stale() -> None:
|
|
rec = _Recorder()
|
|
prs = [DraftPr(number=7, updated_at="garbage")]
|
|
report = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
now=_NOW,
|
|
)
|
|
assert report.stale_reminded == 0
|
|
assert rec.reminders == []
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Side-effect isolation + watermark-not-advanced-on-error
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_alarm_side_effect_error_is_isolated_and_retried() -> None:
|
|
"""A raising on_alarm yields ERRORED and does NOT advance the watermark."""
|
|
calls: list[int] = []
|
|
|
|
def boom(opened_in_window: int) -> None:
|
|
calls.append(opened_in_window)
|
|
raise RuntimeError("slack down")
|
|
|
|
mem = MonitorMemory()
|
|
prs = _open_burst(5)
|
|
report = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=boom,
|
|
on_stale_reminder=lambda pr: None,
|
|
memory=mem,
|
|
now=_NOW,
|
|
)
|
|
assert report.errored == 1
|
|
assert report.alarmed == 0
|
|
# Watermark not advanced -> a retry the very next tick (no cooldown lock-in).
|
|
assert mem.last_alarm_at is None
|
|
report2 = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=boom,
|
|
on_stale_reminder=lambda pr: None,
|
|
memory=mem,
|
|
now=_NOW + timedelta(seconds=30),
|
|
)
|
|
assert report2.errored == 1
|
|
assert len(calls) == 2
|
|
|
|
|
|
def test_stale_side_effect_error_does_not_abort_sweep() -> None:
|
|
"""A reminder that raises for one PR does not stop the runaway ALARM."""
|
|
rec = _Recorder()
|
|
|
|
def boom(pr: DraftPr) -> None:
|
|
raise RuntimeError("slack down")
|
|
|
|
prs = _open_burst(5) + [DraftPr(number=50, updated_at=_ago(days=10))]
|
|
report = run_draft_pr_monitor(
|
|
prs,
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=boom,
|
|
now=_NOW,
|
|
)
|
|
# Runaway still alarms; the stale reminder errored but was isolated.
|
|
assert report.alarmed == 1
|
|
assert report.errored == 1
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Never auto-closes / self-stops; memory hygiene
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_monitor_takes_no_infrastructure_action() -> None:
|
|
"""The only effects are the two injected notify callables — nothing else.
|
|
|
|
There is no close/stop/dispatch seam on the monitor; this asserts the sweep
|
|
exposes ONLY the alarm + reminder hooks (a regression guard against adding a
|
|
self-acting side effect).
|
|
"""
|
|
import inspect
|
|
|
|
sig = inspect.signature(run_draft_pr_monitor)
|
|
effect_params = {p for p in sig.parameters if p.startswith("on_")}
|
|
assert effect_params == {"on_alarm", "on_stale_reminder"}
|
|
|
|
|
|
def test_memory_prunes_gone_prs() -> None:
|
|
"""A PR no longer present has its backoff watermark pruned (bounded growth)."""
|
|
rec = _Recorder()
|
|
mem = MonitorMemory()
|
|
|
|
run_draft_pr_monitor(
|
|
[DraftPr(number=7, updated_at=_ago(days=10))],
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=_NOW,
|
|
)
|
|
assert 7 in mem.last_reminded_at
|
|
|
|
# Next pass: PR #7 is gone (merged/closed by a human). Its watermark prunes.
|
|
run_draft_pr_monitor(
|
|
[],
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
memory=mem,
|
|
now=_NOW + timedelta(minutes=1),
|
|
)
|
|
assert 7 not in mem.last_reminded_at
|
|
|
|
|
|
def test_no_draft_prs_is_quiet() -> None:
|
|
rec = _Recorder()
|
|
report = run_draft_pr_monitor(
|
|
[],
|
|
on_alarm=rec.on_alarm,
|
|
on_stale_reminder=rec.on_stale_reminder,
|
|
now=_NOW,
|
|
)
|
|
assert report.outcomes == []
|
|
assert rec.alarms == []
|
|
assert rec.reminders == []
|