From 74a397e31a0618836c2714a1afce31f80f83111a Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 5 Nov 2026 01:28:02 -0500 Subject: [PATCH 1/2] revert(pr-merge): drop the wake's unmoved-refusal skip (#13107), which never fired in 24 h This reverts the refusal-stands skip from commit 55f8a62854bb58ac7ff74fc8f4f44e2af4ed372b. Its own 24 h gate failed: in 62 wakes, 0 skipped on the probe and 29 paid it for nothing. Removed: the keep-report step's refusal-stands probe, its notice and output, the REFUSAL_STANDS outcome, babysit_verify_evidence.py refusal-stands, babysit_postmerge.py content_refusal, refusal_moved and CONTENT_REFUSALS, the running option of _verifying_runs_unchanged and idle_runs_refusal (only refusal_moved passed it), their tests and the pr-pipeline README sentence. Kept: the judgedRuns reading added later (judged, moved(), _judged_runs), and the same commit's separate run-cache and attempts wiring (BABYSIT_RUN_CACHE in keep-report, attempts.json kept beside the fresh report, the arm step's BABYSIT_POSTMERGE_ATTEMPTS) with its tests. Co-Authored-By: Claude Opus 5.5 --- ...revert-13107-wake-unmoved-skip-e2879703.md | 5 + ...revert-13107-wake-unmoved-skip-f401469e.md | 7 + .github/scripts/pr-merge/workflow.test.mjs | 82 +----- .github/workflows/pr-merge.yml | 24 +- docs/conventions/pr-pipeline/README.md | 6 +- .../babysit-prs/scripts/babysit_postmerge.py | 107 +------ .../scripts/babysit_verify_evidence.py | 56 +--- .../scripts/tests/test_babysit_lane_room.py | 268 ------------------ 8 files changed, 38 insertions(+), 517 deletions(-) create mode 100644 .changes/pr-pipeline/revert-13107-wake-unmoved-skip-e2879703.md create mode 100644 .changes/source-control/revert-13107-wake-unmoved-skip-f401469e.md diff --git a/.changes/pr-pipeline/revert-13107-wake-unmoved-skip-e2879703.md b/.changes/pr-pipeline/revert-13107-wake-unmoved-skip-e2879703.md new file mode 100644 index 00000000000..f7c3f6abb4a --- /dev/null +++ b/.changes/pr-pipeline/revert-13107-wake-unmoved-skip-e2879703.md @@ -0,0 +1,5 @@ +--- +bump: none +--- + +No release: pr-merge's PR-scoped wake no longer probes whether the last sweep's refusing report still stands before its fresh post-merge check, because in 24 hours the probe never skipped a check and only added calls; the arm step's shared run cache and kept attempts stay, so only the wake's GitHub calls change. diff --git a/.changes/source-control/revert-13107-wake-unmoved-skip-f401469e.md b/.changes/source-control/revert-13107-wake-unmoved-skip-f401469e.md new file mode 100644 index 00000000000..8693334d778 --- /dev/null +++ b/.changes/source-control/revert-13107-wake-unmoved-skip-f401469e.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Removed + +- `babysit_verify_evidence.py refusal-stands` is gone, with `babysit_postmerge.py`'s `content_refusal` and `refusal_moved` and the `running` option of the idle tick's verifying-runs read: pr-merge's PR-scoped wake no longer asks whether the last sweep's refusing report still stands, since over its first 24 hours the probe never let a wake skip its fresh check. The idle tick's `judgedRuns` reading is unchanged. diff --git a/.github/scripts/pr-merge/workflow.test.mjs b/.github/scripts/pr-merge/workflow.test.mjs index 7b2fa1cdf24..da91d211cec 100644 --- a/.github/scripts/pr-merge/workflow.test.mjs +++ b/.github/scripts/pr-merge/workflow.test.mjs @@ -573,13 +573,11 @@ test("pr-merge takes a pr input, runs every 30 minutes and names a PR-scoped run // the post-merge check fresh, as post-merge-dispatch does on a refused idle // tick, and keeps the fresh report only when the check passed (exit 0 or 5) // and settle --idle-tick --fresh keeps it too (the idle rule less the -// verifying runs the check just read). A last report that refuses on its own -// content with nothing it read moved (refusal-stands exits 0) is removed with -// no fresh check. Anything else removes it, so the arm step is skipped and -// nothing arms. +// verifying runs the check just read). Anything else removes it, so the arm +// step is skipped and nothing arms. const KEEP_STEP = "Keep the last sweep's report only while nothing it read has moved"; const FRESH_SHA = "f".repeat(40); -const keepReport = ({ code = "0", settles = [0], standing = 1, fresh = 0, files = true, attempts = false } = {}) => { +const keepReport = ({ code = "0", settles = [0], fresh = 0, files = true, attempts = false } = {}) => { const arm = job("enable-auto-merge"); const body = new RegExp(`- name: ${KEEP_STEP.replace(/[.']/g, "\\$&")}\\n([\\s\\S]*?)(?=\\n {6}- name: )`).exec(arm)?.[1]; assert.ok(body, "a PR-scoped run settles the last sweep's report"); @@ -590,8 +588,7 @@ const keepReport = ({ code = "0", settles = [0], standing = 1, fresh = 0, files const args = join(dir, "args"); writeFileSync(args, ""); // Each settle call takes the next exit code from `settles` and prints - // settle's JSON; refusal-stands exits `standing`; the fresh check prints a - // marked report and exits `fresh`. + // settle's JSON; the fresh check prints a marked report and exits `fresh`. writeFileSync(join(dir, "settles"), settles.join("\n") + "\n"); writeFileSync( join(bin, "python3"), @@ -600,11 +597,6 @@ echo "$@" >>'${args}' case "$1" in *babysit_postmerge.py) echo '{"fresh":true}'; exit ${fresh} ;; esac -if [ "$2" = refusal-stands ]; then - if [ ${standing} = 0 ]; then reason=null; else reason='"head-moved"'; fi - echo "{\\"stands\\": $([ ${standing} = 0 ] && echo true || echo false), \\"refusal\\": \\"needs-dispatch\\", \\"computedAt\\": \\"2026-10-01T00:00:00+00:00\\", \\"reason\\": $reason}" - exit ${standing} -fi next=$(head -n1 '${join(dir, "settles")}'); sed -i 1d '${join(dir, "settles")}' if [ "\${next:-1}" = 0 ]; then echo '{"reuse": true, "reason": null}'; else echo '{"reuse": false, "reason": "idle tick needs-dispatch"}'; fi exit "\${next:-1}" @@ -657,15 +649,12 @@ const SETTLE = (sha, fresh = "") => const SETTLE_FRESH = (sha) => SETTLE(sha, " --fresh"); const FRESH_CHECK = /^\.base\/plugins\/source-control\/skills\/babysit-prs\/scripts\/babysit_postmerge\.py --repo o\/r --agent-identity lanes-bot --lane-merges-only$/; -// Whether the last report's own refusal stands, at the commit whose code made it. -const REFUSAL_STANDS = - /^\.base\/plugins\/source-control\/skills\/babysit-prs\/scripts\/babysit_verify_evidence\.py refusal-stands --repo o\/r --postmerge-result \S+\/post-merge\/post-merge\.json --code-sha c{40}$/; test("a PR-scoped run keeps the last sweep's report only when settle --idle-tick keeps it", () => { const kept = keepReport(); assert.equal(kept.status, 0); assert.equal(kept.kept, true); - assert.equal(kept.output, "stands=true\nfresh=\nfresh-reason=\nrefusal-stands=\n"); + assert.equal(kept.output, "stands=true\nfresh=\nfresh-reason=\n"); assert.match(kept.body, /^ {8}id: keep-report\n/); // The same call post-merge-dispatch makes for a reused report, and no // fresh check. @@ -683,21 +672,17 @@ test("a PR-scoped run keeps the last sweep's report only when settle --idle-tick // check runs, then settle --idle-tick --fresh on the fresh report at this checkout's // commit, and only both passing lets the arm step run on it. test("a PR-scoped run whose report does not stand runs the post-merge check fresh and keeps it when both checks pass", () => { - // `before`: the calls on the last report ahead of the fresh check. A last - // report whose refusal could stand (exit 0, 1 or 5) is asked, and here - // something it read has moved; an unjudged or missing one is not. - const LAST_SETTLE = SETTLE("c{40}"); - for (const [why, options, before] of [ - ["settle refuses the last report and it has moved", { settles: [1, 0] }, [LAST_SETTLE, REFUSAL_STANDS]], - ["the last check could not judge", { code: "3", settles: [0] }, []], - ["the last check failed and it has moved", { code: "1", settles: [0] }, [REFUSAL_STANDS]], - ["no last report", { files: false, settles: [0] }, []], - ["the fresh check finds a merge not yet verified", { settles: [1, 0], fresh: 5 }, [LAST_SETTLE, REFUSAL_STANDS]], + for (const [why, options] of [ + ["settle refuses the last report", { settles: [1, 0] }], + ["the last check could not judge", { code: "3", settles: [0] }], + ["the last check failed", { code: "1", settles: [0] }], + ["no last report", { files: false, settles: [0] }], + ["the fresh check finds a merge not yet verified", { settles: [1, 0], fresh: 5 }], ]) { const run = keepReport(options); const fresh = String(options.fresh ?? 0); assert.equal(run.status, 0, why); - assert.equal(run.output, `fresh-started=true\nstands=true\nfresh=${fresh}\nfresh-reason=\nrefusal-stands=\n`, why); + assert.equal(run.output, `fresh-started=true\nstands=true\nfresh=${fresh}\nfresh-reason=\n`, why); assert.equal(run.kept, true, why); assert.equal(run.json, '{"fresh":true}\n', `${why}: the room is the fresh report, not the last one`); assert.equal(run.codeFile, `${fresh}\n`, why); @@ -706,42 +691,8 @@ test("a PR-scoped run whose report does not stand runs the post-merge check fres assert.ok(check >= 0, `${why}: the fresh check ran`); assert.match(run.calls[check + 1] ?? "", SETTLE_FRESH(FRESH_SHA), `${why}: settle --idle-tick --fresh checked the fresh report`); // The last report's settle, when it ran, never skips the runs test. - assert.equal(check, before.length, why); - before.forEach((call, i) => assert.match(run.calls[i], call, why)); + if (check > 0) assert.match(run.calls[0], SETTLE("c{40}"), why); assert.equal(run.calls.length, check + 2, why); - // What moved is logged whenever the refusal was asked about. - assert.equal(run.stdout.includes('"reason": "head-moved"'), before.includes(REFUSAL_STANDS), why); - } -}); - -// A last report that refuses on what it found, with nothing it read moved, -// would be refused by a fresh check too: none runs, the report is removed and -// the arm step stays skipped, with a notice naming the report and why. -test("a PR-scoped run whose last report's refusal stands runs no fresh check and arms nothing", () => { - for (const [why, options, before] of [ - ["an idle refusal on its content", { settles: [1], standing: 0 }, [SETTLE("c{40}"), REFUSAL_STANDS]], - ["a failed check", { code: "1", standing: 0 }, [REFUSAL_STANDS]], - ["a check not yet verified that needs a full run", { code: "5", settles: [1], standing: 0 }, [SETTLE("c{40}"), REFUSAL_STANDS]], - ]) { - const run = keepReport(options); - assert.equal(run.status, 0, why); - assert.equal(run.kept, false, `${why}: no report is left for the arm step`); - assert.equal(run.output, "stands=false\nfresh=\nfresh-reason=\nrefusal-stands=needs-dispatch\n", why); - assert.equal(run.calls.length, before.length, `${why}: no fresh check`); - before.forEach((call, i) => assert.match(run.calls[i], call, why)); - assert.match( - run.stdout, - new RegExp( - `::notice::the last sweep's post-merge report \\(exit ${options.code ?? 0}, computed 2026-10-01T00:00:00\\+00:00\\) refuses on its own content \\(needs-dispatch\\) and nothing it read has moved since`, - ), - why, - ); - } - // An unjudged report is never asked: its refusal is a failed read. - for (const code of ["3", "75"]) { - const run = keepReport({ code, standing: 0 }); - assert.ok(!run.calls.some((c) => REFUSAL_STANDS.test(c)), `exit ${code}`); - assert.ok(run.calls.some((c) => FRESH_CHECK.test(c)), `exit ${code}: the fresh check ran`); } }); @@ -774,7 +725,7 @@ test("a PR-scoped run whose fresh check or fresh settle refuses keeps no report" const run = keepReport(options); assert.equal(run.status, 0, why); assert.equal(run.kept, false, why); - assert.equal(run.output, `fresh-started=true\nstands=false\nfresh=${options.fresh}\nfresh-reason=${reason}\nrefusal-stands=\n`, why); + assert.equal(run.output, `fresh-started=true\nstands=false\nfresh=${options.fresh}\nfresh-reason=${reason}\n`, why); assert.ok(run.calls.some((c) => FRESH_CHECK.test(c)), `${why}: the fresh check ran`); assert.equal(run.calls.some((c) => SETTLE_FRESH(FRESH_SHA).test(c)), settled, why); assert.match(run.stdout, new RegExp(`::notice::the fresh post-merge check \\(exit ${options.fresh}\\) does not stand for this run`), why); @@ -815,11 +766,6 @@ test("a PR-scoped run's outcome names the fresh check's refusal and which report assert.match(wakeOutcome({ ...base, FRESH: "5" }), /refused the fresh post-merge report \(no reason read\)$/); assert.match(wakeOutcome({ ...base, STANDS: "true", FRESH: "0", OUTCOME: "armed" }), /report=fresh outcome=armed$/); assert.match(wakeOutcome({ ...base, STANDS: "true", OUTCOME: "armed" }), /report=last outcome=armed$/); - // A last report whose own refusal stood: no fresh check ran. - assert.match( - wakeOutcome({ ...base, REFUSAL_STANDS: "needs-dispatch" }), - /report=last outcome=skipped: the last sweep's report still refuses \(needs-dispatch\) and nothing it read has moved$/, - ); // A fresh check that timed out or was cancelled writes no stands or fresh output. assert.match( wakeOutcome({ KILL: "true", WHO: "true", FRESH_STARTED: "true" }), diff --git a/.github/workflows/pr-merge.yml b/.github/workflows/pr-merge.yml index 6c54255d6f2..1806f1fc8a4 100644 --- a/.github/workflows/pr-merge.yml +++ b/.github/workflows/pr-merge.yml @@ -2499,13 +2499,7 @@ jobs: # 5) and `settle --idle-tick` keeps, so a verifying run or canary that # moved since its computedAt, changed quarantine markers or pins, or # any hold the report carries refuses it (postmerge.idle_refusal). - # A report that refuses on what it found (exit 1, or an idle refusal - # such as needs-dispatch, not its age or a failed read) while nothing - # it read has moved, no verifying run created or re-run since included - # (`refusal-stands`), stands only as that refusal: a fresh check would - # refuse too, so none runs, and the step removes it, so the wake arms - # and disarms nothing, as on a refused fresh check. No other report - # that does not stand is reused: the step removes it + # A report that does not stand is never reused: the step removes it # and runs the sweep's post-merge check fresh, as post-merge-dispatch # does when its idle tick refuses, with this job's read-only token (the # check writes nothing without --remediate). The fresh report stands @@ -2541,7 +2535,7 @@ jobs: set -uo pipefail last=$RUNNER_TEMP/post-merge scripts=.base/plugins/source-control/skills/babysit-prs/scripts - kept=false fresh='' reason='' refusal='' standing='' last_code='' last_sha='' + kept=false fresh='' reason='' settle() { python3 "$scripts/babysit_verify_evidence.py" settle --idle-tick --repo "$REPO" \ --postmerge-result "$last/post-merge.json" --code-sha "$@" @@ -2555,17 +2549,7 @@ jobs: fi if [ "$kept" = true ]; then echo "the last sweep's post-merge report still stands: nothing it reads has moved" - elif [[ $last_code =~ ^[015]$ ]] && - standing=$(python3 "$scripts/babysit_verify_evidence.py" refusal-stands --repo "$REPO" \ - --postmerge-result "$last/post-merge.json" --code-sha "$last_sha"); then - refusal=$(jq -r '.refusal // ""' <<<"$standing" 2>/dev/null) || refusal='' - refusal=${refusal:-unnamed} - computed=$(jq -r '.computedAt // ""' <<<"$standing" 2>/dev/null) || computed='' - rm -rf "${last:?}" - echo "::notice::the last sweep's post-merge report (exit $last_code, computed $computed) refuses on its own content ($refusal) and nothing it read has moved since, so a fresh check would refuse it too and is not run; nothing is armed or disarmed, and the next sweep checks again" else - # Why the last report's refusal does not stand, when it refused. - [ -z "$standing" ] || echo "$standing" [ ! -f "$last/attempts.json" ] || cp "$last/attempts.json" "$BABYSIT_POSTMERGE_ATTEMPTS" rm -rf "${last:?}" mkdir -p "$last" @@ -2603,7 +2587,6 @@ jobs: echo "stands=$kept" echo "fresh=$fresh" echo "fresh-reason=${reason//[$'\r\n']/ }" - echo "refusal-stands=${refusal//[$'\r\n']/ }" } >>"$GITHUB_OUTPUT" # While merge.report-only is anything but false it reports each @@ -2770,7 +2753,6 @@ jobs: FRESH: ${{ steps.keep-report.outputs.fresh }} FRESH_REASON: ${{ steps.keep-report.outputs.fresh-reason }} FRESH_STARTED: ${{ steps.keep-report.outputs.fresh-started }} - REFUSAL_STANDS: ${{ steps.keep-report.outputs.refusal-stands }} CALLS: ${{ steps.budget-end.outputs.calls }} run: | if [ -z "$OUTCOME" ]; then @@ -2780,8 +2762,6 @@ jobs: OUTCOME="skipped: the who step stopped it" elif [ "$SUPERSEDED" = true ]; then OUTCOME="skipped: a waiting run does this work" - elif [ "$STANDS" = false ] && [ -n "$REFUSAL_STANDS" ]; then - OUTCOME="skipped: the last sweep's report still refuses ($REFUSAL_STANDS) and nothing it read has moved" elif [ -z "$FRESH" ] && [ "$FRESH_STARTED" = true ]; then OUTCOME="skipped: the fresh post-merge check did not finish (timeout or cancel)" elif [ "$STANDS" = false ] && [ "$FRESH" = 75 ]; then diff --git a/docs/conventions/pr-pipeline/README.md b/docs/conventions/pr-pipeline/README.md index 500cfede932..e6fc4cbb1d4 100644 --- a/docs/conventions/pr-pipeline/README.md +++ b/docs/conventions/pr-pipeline/README.md @@ -1980,10 +1980,8 @@ reads the pull request once and, for a lanes App one that is not already armed a `do-not-merge`, dispatches a PR-scoped `pr-merge.yml` run (`-f pr=`, run name `pr-merge: arm #`) that reads and arms only that pull request, its lane room from the last sweep's post-merge report, and skips the merge job, `post-merge-dispatch` and every other sweep job. That report stands only -while `settle --idle-tick` keeps it. When it refuses on what it found and nothing it read has -moved, no verifying run created or re-run since included (`refusal-stands`), the wake refuses with -it and arms nothing, since a fresh check would refuse too. Otherwise the wake never reuses it -but runs the post-merge check fresh on its own read-only token, and arms only when that check passes (exit 0 +while `settle --idle-tick` keeps it; when it does not, the wake never reuses it but runs the +post-merge check fresh on its own read-only token, and arms only when that check passes (exit 0 or 5) and `settle --idle-tick` keeps the fresh report too, since the wake starts no full run or re-run. Its `api-calls` totals, which report the room came from (`report=last`, `fresh` or `none`) and the outcome land in one `pr-merge-wake` notice. The 30-minute schedule is the backstop that arms diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py index c42f90bec55..5583370361b 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py @@ -4414,33 +4414,28 @@ def _verifying_runs_unchanged( *, created: datetime, since: datetime, - running: bool = False, judged: Mapping[int, datetime] | None = None, ) -> bool: """Whether no full or canary run on `branch` created at or after `created` completed, or was re-run, at or after `since`, from one listing per verifying workflow: every workflow's runs on a busy branch fill more than one page in an hour or two. A run started since and still running - changes nothing yet, unless `running`: then any run created, started - again or updated since counts, whatever its status, since a fresh check - reads a red run re-run since as pending. A completed run `judged` holds - at the `updated_at` it reads now (`_judged_runs`) is one the check - already read finished, so it counts for nothing; a re-run moves its - `updated_at`. A listing that is not whole raises `PostMergeReadError`.""" + changes nothing yet. A completed run `judged` holds at the `updated_at` + it reads now (`_judged_runs`) is one the check already read finished, so + it counts for nothing; a re-run moves its `updated_at`. A listing that is + not whole raises `PostMergeReadError`.""" def moved(run: dict[str, Any]) -> bool: if run.get("path") not in VERIFYING_WORKFLOWS: return False - completed = run.get("status") == "completed" - if not (running or completed): + if run.get("status") != "completed": return False updated = _time(run.get("updated_at")) if updated < since: return False run_id = run.get("id") return not ( - completed - and judged is not None + judged is not None and isinstance(run_id, int) and judged.get(run_id) == updated ) @@ -4667,11 +4662,9 @@ def idle_runs_refusal( branch: str, *, confirm_wait_minutes: int, - running: bool = False, ) -> str | None: """`idle_refusal`'s verifying runs, in two one-page listings - (`_verifying_runs_unchanged`, with `running`), for a report - `idle_report_refusal` passed.""" + (`_verifying_runs_unchanged`), for a report `idle_report_refusal` passed.""" report = shared.gate.report try: created = shared.computed_at - ( @@ -4688,7 +4681,6 @@ def idle_runs_refusal( branch, created=created, since=since, - running=running, judged=_judged_runs(report), ): return "runs-changed" @@ -4713,91 +4705,6 @@ def idle_marks_refusal(repo: str, shared: policy.SharedPostMerge) -> str | None: return None -# `idle_report_refusal`'s refusals a check reading the same inputs gives -# again: what the report found, not its age, a deadline or a failed read. A -# hold comes only with an unreadable report, and `reruns` can lapse with -# nothing moved once its red run leaves the `_lane_reruns` lookback. -CONTENT_REFUSALS = frozenset( - {"needs-dispatch", "unattributed", "quarantines", "excused"} -) - - -def content_refusal( - shared: policy.SharedPostMerge, *, now: datetime | None = None -) -> str | None: - """Why a readable report refuses on what it found (`failing` for exit 1, - else one of `CONTENT_REFUSALS`), or None: it passes the idle tick, its - refusal is age or time (`stale-soon`, `dispatch-deadline`), or it lacks - what `refusal_moved` reads again (pins, `outstandingSince`, a - `_lane_reruns` read that did not fail), or it asks for a re-run. Reads - nothing.""" - report = shared.gate.report - if ( - report.get("readable") is not True - or json_array(report.get("reruns")) - or not is_json_object(report.get("pins")) - or "outstandingSince" not in report - or any( - str(w).startswith("lane-rerun-unreadable") - for w in json_array(report.get("warnings")) - ) - ): - return None - if report.get("ok") is not True: - return "failing" - refusal = idle_report_refusal(shared, now=now) - return refusal if refusal in CONTENT_REFUSALS else None - - -def refusal_moved( - repo: str, - shared: policy.SharedPostMerge, - branch: str, - *, - identity: str, - confirm_wait_minutes: int, - now: datetime | None = None, -) -> str | None: - """What a report refusing on its content (`content_refusal`) was read - from that has moved since, or None when a check run now would read the - same and refuse it too: no older than `SHARED_MAX_AGE` (else `stale`), - bound to `identity` and the lane, the default branch head, its config - commit and the demotion unchanged (`policy._moved`, a tripped demotion - probe read again), no verifying run created, re-run or updated since - (`idle_runs_refusal` with `running`), and the quarantine issues and pins - as they were (`idle_marks_refusal`). A failed read is moved. Past - `computedAt` the check's time rules only refuse more: a pending merge - past `DISPATCH_DEADLINE` or an expired excuse fails.""" - age = (now or datetime.now(UTC)) - shared.computed_at - if not timedelta(0) <= age <= policy.SHARED_MAX_AGE: - return "stale" - if _login_key(shared.identity) != _login_key(identity): - return "identity-differs" - if not shared.lane_merges_only: - return "lane-differs" - try: - moved = policy._moved( - repo, - branch, - identity, - shared, - lambda answer, default: ( - idle_runs_refusal( - repo, - answer, - default, - confirm_wait_minutes=confirm_wait_minutes, - running=True, - ) - is None - ), - reread_demotion=True, - ) - except (RuntimeError, ValueError, OSError) as exc: - return f"unreadable: {exc}" - return moved or idle_marks_refusal(repo, shared) - - def read_shared(path: str) -> policy.SharedPostMerge | None: """The run's shared answer from this script's own report at `path`, or None when it is missing, malformed, unreadable as a verdict, or has no diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py index 855b5c7b50b..8f2b53531d8 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py @@ -4811,49 +4811,6 @@ def idle_tick(refusal: str | None) -> str | None: ) -def _refusal_stands(repo: str, path: str, code_sha: str) -> int: - """`refusal-stands`: whether an earlier tick's post-merge report at `path`, - its check run by the code at commit `code_sha`, refuses on what it found - (`postmerge.content_refusal`) with nothing it was read from moved since - (`postmerge.refusal_moved`), so a check run now would refuse too and need - not run. The content is judged before any read. Prints `{"stands", - "refusal", "computedAt", "reason"}`, `reason` why it does not stand; - exit 0 when it stands, else 1, and the caller runs the check.""" - shared = postmerge.read_shared(path) - refusal: str | None = None - reason: str | None - if shared is None: - reason = "post-merge answer missing or unreadable" - elif not merge_policy.check_code_unchanged(code_sha): - reason = "post-merge answer code-changed" - elif (refusal := postmerge.content_refusal(shared)) is None: - reason = "no refusal of its own content" - else: - try: - default = merge_policy._default_branch(repo, shared=True) - config = merge_policy._default_branch_config(repo, default) - reason = postmerge.refusal_moved( - repo, - shared, - default, - identity=config.agent_identity, - confirm_wait_minutes=config.confirm_wait_minutes, - ) - except (RuntimeError, ValueError, OSError) as exc: - reason = f"unreadable: {exc}" - print( - json.dumps( - { - "stands": reason is None, - "refusal": refusal, - "computedAt": shared and shared.computed_at.isoformat(), - "reason": reason, - } - ) - ) - return 0 if reason is None else 1 - - # GitHub refuses a comment body over 65536 characters; the findings are cut # to leave room for the framing (judgment). FINDINGS_MAX = 60000 @@ -6132,12 +6089,7 @@ def main(argv: list[str] | None = None, *, now: datetime | None = None) -> int: again (`_settle`). With `--idle-tick`, post-merge-dispatch's reuse of an earlier tick's report, which must also pass `postmerge.idle_refusal`; `--fresh` with it, for a report this job's check just made, skips that - rule's verifying runs (`idle_runs_refusal`). - - `refusal-stands --repo R --postmerge-result --code-sha `: - whether an earlier tick's report refuses on its own content with nothing - it read moved since, so pr-merge's PR-scoped wake refuses as it does - without running the check (`_refusal_stands`).""" + rule's verifying runs (`idle_runs_refusal`).""" parser = argparse.ArgumentParser( prog="babysit_verify_evidence.py", allow_abbrev=False ) @@ -6177,10 +6129,6 @@ def main(argv: list[str] | None = None, *, now: datetime | None = None) -> int: settle.add_argument("--code-sha", required=True) settle.add_argument("--idle-tick", action="store_true") settle.add_argument("--fresh", action="store_true") - stands = sub.add_parser("refusal-stands", allow_abbrev=False) - stands.add_argument("--repo", required=True) - stands.add_argument("--postmerge-result", required=True) - stands.add_argument("--code-sha", required=True) legacy = sub.add_parser("legacy-fails", allow_abbrev=False) legacy.add_argument("--repo", required=True) legacy.add_argument("--branch", default="main") @@ -6218,8 +6166,6 @@ def main(argv: list[str] | None = None, *, now: datetime | None = None) -> int: idle=args.idle_tick, fresh=args.fresh, ) - if args.command == "refusal-stands": - return _refusal_stands(args.repo, args.postmerge_result, args.code_sha) if args.command == "lane-room": shared = postmerge.read_shared(args.postmerge_result) answer = merge_policy.lane_room( diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_lane_room.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_lane_room.py index 82b838e87f8..d6e6fe0f773 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_lane_room.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_lane_room.py @@ -1333,274 +1333,6 @@ def test_a_pin_that_reads_differently_refuses(self) -> None: self.assertEqual(failed, "unreadable: #5 pin: 502") -MERGED = FLOOR - timedelta(hours=1) -# The last sweep's report refusing on what it found: lane merge #5, merged an -# hour before, has no full run covering it yet. -REFUSING = { - **SETTLED, - "needsDispatch": True, - "autoMerges": [{"number": 5, "verified": False, "mergedAt": MERGED.isoformat()}], -} -# The same report from a check that exited 1: a red run charged to #5. -FAILING = { - **REFUSING, - "ok": False, - "needsDispatch": False, - "failures": [{"signal": "dispatch", "number": 5}], -} - - -class RefusalStands(unittest.TestCase): - """`content_refusal` and `refusal_moved`: a wake skips its fresh check - only when the last report refuses on its own content and nothing a fresh - check reads has moved. Expected values are the brief's three conditions: - a content refusal (not age, not a failed read), main's tip and the - idle tick's moved evidence unchanged, and no verifying run created, re-run - or still running since computedAt.""" - - def moved( - self, - report: dict[str, object] = REFUSING, - *, - now: datetime = NOW, - head: str = MAIN, - runs: list[dict[str, object]] = (), # type: ignore[assignment] - issues: list[dict[str, object]] = (), # type: ignore[assignment] - pins: dict[int, str | None] | None = None, - demotion_probe: bool = False, - demotion: policy.Demotion | None = None, - identity: str = IDENTITY, - ) -> str | None: - self.calls: list[str] = [] - - def read(args: list[str], label: str) -> object: - self.calls.append(args[1]) - if "/actions/workflows/" in args[1]: - return {"total_count": len(runs), "workflow_runs": list(runs)} - return list(issues) - - answer = shared(report=report) - with ( - mock.patch.object(pm, "_read", side_effect=read), - mock.patch.object( - pm, "read_pins", return_value={5: SHA_PINNED} if pins is None else pins - ), - mock.patch.object( - policy, "_default_branch_head", return_value=("main", head) - ), - mock.patch.object(policy, "newest_config_commit", return_value=MAIN), - mock.patch.object(policy, "_demotion_changed", return_value=demotion_probe), - mock.patch.object(policy, "permission_reader"), - mock.patch.object( - policy, "read_demotion", return_value=demotion or answer.demotion - ) as self.read_demotion, - ): - return pm.refusal_moved( - REPO, - answer, - "main", - identity=identity, - confirm_wait_minutes=CONFIRM_WAIT, - now=now, - ) - - def test_a_refusal_on_its_content_is_named(self) -> None: - self.assertEqual( - pm.content_refusal(shared(report=REFUSING), now=NOW), "needs-dispatch" - ) - self.assertEqual(pm.content_refusal(shared(report=FAILING), now=NOW), "failing") - for reason, change in ( - ("unattributed", {"unattributed": ["https://run/1/attempts/1"]}), - ("quarantines", {"quarantines": [{"suite": "x"}]}), - ): - with self.subTest(reason): - report = {**SETTLED, **change} - self.assertEqual( - pm.content_refusal(shared(report=report), now=NOW), reason - ) - - def test_no_refusal_of_its_own_content_is_none(self) -> None: - cases = { - "it passes": SETTLED, - "unjudged (exit 3)": {**REFUSING, "readable": False, "ok": False}, - "it asks for a re-run": {**REFUSING, "reruns": [{"runId": 9}]}, - "a lane re-run read failed": { - **REFUSING, - "warnings": ["lane-rerun-unreadable: 502"], - }, - "no pins recorded": {k: v for k, v in REFUSING.items() if k != "pins"}, - "no outstandingSince": { - k: v for k, v in REFUSING.items() if k != "outstandingSince" - }, - } - for why, report in cases.items(): - with self.subTest(why): - self.assertIsNone(pm.content_refusal(shared(report=report), now=NOW)) - # Refused only on age: idle_report_refusal says stale-soon. - late = FLOOR + policy.SHARED_MAX_AGE - pm.IDLE_HEADROOM + timedelta(minutes=1) - answer = shared(report=SETTLED) - self.assertEqual(pm.idle_report_refusal(answer, now=late), "stale-soon") - self.assertIsNone(pm.content_refusal(answer, now=late)) - - def test_nothing_moved_lets_the_refusal_stand(self) -> None: - for report in (REFUSING, FAILING): - with self.subTest(report["ok"]): - self.assertIsNone(self.moved(report)) - # Two run listings, one quarantine issue listing. - self.assertEqual(sum("/actions/workflows/" in c for c in self.calls), 2) - self.read_demotion.assert_not_called() - # A run that finished before computedAt (less the clock margin) moved - # nothing. - before = FLOOR - policy.SINCE_CLOCK_MARGIN - timedelta(seconds=1) - done = run( - "pr-require-checks.yml", status="completed", updated=before, created=MERGED - ) - self.assertIsNone(self.moved(runs=[done])) - - def test_main_moved_runs_the_check(self) -> None: - self.assertEqual(self.moved(head=NEW_MAIN), "head-moved") - self.assertEqual(self.calls, []) - - def test_a_verifying_run_created_or_re_run_since_runs_the_check(self) -> None: - after = FLOOR + timedelta(minutes=1) - cases = { - "created since, still running": run( - "pr-require-checks.yml", - status="in_progress", - updated=after, - created=after, - ), - "created since, queued": run( - "post-merge-detect-silent-revert.yml", - status="queued", - updated=after, - created=after, - ), - "created before, re-run since and running": run( - "pr-require-checks.yml", - status="in_progress", - updated=after, - created=MERGED, - ), - "completed since": run( - "pr-require-checks.yml", - status="completed", - updated=after, - created=MERGED, - ), - } - for why, row in cases.items(): - with self.subTest(why): - self.assertEqual(self.moved(runs=[row]), "runs-moved") - - def test_a_run_the_check_read_finished_moves_nothing(self) -> None: - # In the clock margin, so moved without the record; with the report - # holding it at the same updated_at (FLOOR less 30 s), nothing moved. - done = run( - "pr-require-checks.yml", - status="completed", - updated=FLOOR - timedelta(seconds=30), - created=MERGED, - run_id=77, - ) - self.assertEqual(self.moved(runs=[done]), "runs-moved") - recorded = {**REFUSING, "judgedRuns": {"77": "2026-09-30T23:59:30Z"}} - self.assertIsNone(self.moved(recorded, runs=[done])) - # Still running when listed now: the record holds completed runs only. - rerun = run( - "pr-require-checks.yml", - status="in_progress", - updated=FLOOR - timedelta(seconds=30), - created=MERGED, - run_id=77, - ) - self.assertEqual(self.moved(recorded, runs=[rerun]), "runs-moved") - - def test_marks_or_demotion_that_changed_run_the_check(self) -> None: - marked = {"number": 3, "body": ""} - self.assertEqual(self.moved(issues=[marked]), "quarantine-changed") - self.assertEqual(self.moved(pins={5: None}), "pins-changed") - # A tripped demotion probe is read again: the same demotion stands. - self.assertIsNone(self.moved(demotion_probe=True)) - self.read_demotion.assert_called_once() - other = policy.Demotion(True, "record-open", (7,)) - self.assertEqual( - self.moved(demotion_probe=True, demotion=other), "demotion-changed" - ) - - def test_age_and_binding_bound_it(self) -> None: - self.assertEqual( - self.moved(now=FLOOR + policy.SHARED_MAX_AGE + timedelta(seconds=1)), - "stale", - ) - self.assertIsNone(self.moved(now=FLOOR + policy.SHARED_MAX_AGE)) - self.assertEqual(self.moved(identity="other[bot]"), "identity-differs") - self.assertEqual(self.calls, []) - - def command( - self, report: dict[str, object], *, code_unchanged: bool = True, moved=None - ) -> tuple[int, dict]: - out = io.StringIO() - with ( - mock.patch.object( - ve.postmerge, "read_shared", return_value=shared(report=report) - ), - mock.patch.object( - policy, "check_code_unchanged", return_value=code_unchanged - ), - mock.patch.object(policy, "_default_branch", return_value="main"), - mock.patch.object(policy, "_default_branch_config", return_value=config()), - mock.patch.object( - ve.postmerge, "refusal_moved", return_value=moved - ) as self.refusal_moved, - contextlib.redirect_stdout(out), - ): - code = ve.main( - [ - "refusal-stands", - "--repo", - REPO, - "--postmerge-result", - "/tmp/x.json", - "--code-sha", - MAIN, - ] - ) - return code, json.loads(out.getvalue()) - - def test_the_command_exits_0_only_when_the_refusal_stands(self) -> None: - self.assertEqual( - self.command(REFUSING), - ( - 0, - { - "stands": True, - "refusal": "needs-dispatch", - "computedAt": FLOOR.isoformat(), - "reason": None, - }, - ), - ) - self.refusal_moved.assert_called_once() - self.assertEqual( - self.refusal_moved.call_args.kwargs, - { - "identity": IDENTITY, - "confirm_wait_minutes": config().confirm_wait_minutes, - }, - ) - code, out = self.command(REFUSING, moved="runs-moved") - self.assertEqual((code, out["stands"], out["reason"]), (1, False, "runs-moved")) - # A report with no content refusal, or changed check code, is - # refused before any read. - code, out = self.command(SETTLED) - self.assertEqual((code, out["reason"]), (1, "no refusal of its own content")) - self.refusal_moved.assert_not_called() - code, out = self.command(REFUSING, code_unchanged=False) - self.assertEqual((code, out["reason"]), (1, "post-merge answer code-changed")) - self.refusal_moved.assert_not_called() - - class CheckCodeUnchanged(unittest.TestCase): """`check_code_unchanged`: whether the check's code (the babysit-prs scripts, docs/conventions/pr-pipeline.yaml and the yaml-subset.awk parser From 6922963a460e25282cec78e627fcd3bcfba45ff7 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 5 Nov 2026 01:33:40 -0500 Subject: [PATCH 2/2] test(source-control): pin idle_runs_refusal's judgedRuns reading The revert removed the only test that reached idle_runs_refusal's judged= argument, which sat in the refusal_moved class. The new IdleRefusal case shows a completed run the report recorded at the same updated_at counts as unmoved, while the same run without the record, or re-run since, counts as moved. It fails when the argument is deleted. Co-Authored-By: Claude Opus 5.5 --- .../scripts/tests/test_babysit_lane_room.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_lane_room.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_lane_room.py index d6e6fe0f773..f972bb60bed 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_lane_room.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_lane_room.py @@ -1281,6 +1281,30 @@ def test_a_verifying_run_that_completed_since_refuses(self) -> None: self.assertIsNone(self.refusal(runs=quiet)) self.assertTrue(self.refusal(runs_total=101).startswith("unreadable")) + def test_a_run_the_report_judged_finished_moves_nothing(self) -> None: + # Updated 30 s before the report, inside the 60 s clock margin. + def done(updated: datetime) -> dict[str, object]: + return run( + "pr-require-checks.yml", status="completed", updated=updated, run_id=77 + ) + + judged = FLOOR - timedelta(seconds=30) + self.assertEqual( + self.refusal(runs=[done(judged)]), + "runs-changed", + "with no judgedRuns record the run in the margin counts as moved", + ) + recorded = {**SETTLED, "judgedRuns": {"77": "2026-09-30T23:59:30Z"}} + self.assertIsNone( + self.refusal(recorded, runs=[done(judged)]), + "a run the report judged at the same updated_at counts for nothing", + ) + self.assertEqual( + self.refusal(recorded, runs=[done(NOW)]), + "runs-changed", + "the same run re-run since has a later updated_at and counts", + ) + def test_an_unverified_merge_lists_runs_from_its_merge_and_ends_at_the_deadline( self, ) -> None: