From 6ae9a455c6945861aa0ac181757908e7bf28a908 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:29:28 -0500 Subject: [PATCH 1/2] chore(source-control): drop the verdict reader's unused listing-carry input No workflow sets BABYSIT_VERIFY_LISTING_CARRY, so the reader's carry path and its environment constant are removed. Incremental-listing tests now seed the run's saved listing instead, and the carry-only verify-cap tests are deleted. Co-Authored-By: Claude Opus 5.5 --- ...chore-drop-listing-carry-input-50953b4f.md | 7 + .../scripts/pr-unblock/verify-cap.test.mjs | 21 +- .../scripts/babysit_verify_evidence.py | 189 +++++++++++------- .../tests/test_babysit_verify_evidence.py | 165 ++++++++------- 4 files changed, 210 insertions(+), 172 deletions(-) create mode 100644 .changes/source-control/chore-drop-listing-carry-input-50953b4f.md diff --git a/.changes/source-control/chore-drop-listing-carry-input-50953b4f.md b/.changes/source-control/chore-drop-listing-carry-input-50953b4f.md new file mode 100644 index 00000000000..6d6262d53f5 --- /dev/null +++ b/.changes/source-control/chore-drop-listing-carry-input-50953b4f.md @@ -0,0 +1,7 @@ +--- +bump: none +--- + +### Changed + +- The verdict reader no longer reads a carried artifact listing (`BABYSIT_VERIFY_LISTING_CARRY`); no workflow set it. No behavior changes. diff --git a/.github/scripts/pr-unblock/verify-cap.test.mjs b/.github/scripts/pr-unblock/verify-cap.test.mjs index dc7e53da423..52b1f7b8580 100644 --- a/.github/scripts/pr-unblock/verify-cap.test.mjs +++ b/.github/scripts/pr-unblock/verify-cap.test.mjs @@ -226,9 +226,7 @@ test("a refusal other than the caller limit keeps the waits for later reads", as // Both dispatchers' FAIL-cap reads take the run cache and head-ref map // pr-merge carried, fetched on the job's first FAIL-cap read, not by a step // every run pays for, and share one saved listing across every pull request -// of the job. They fetch no carried listing: the reader reads the FAIL index -// by exact name, so no gate saves one, and looking for it cost 11 calls a job -// (listing-carry's api-calls notice, dispatch job 126277059871 on 2026-11-03). +// of the job. const READER = readFileSync(new URL("../../../plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py", import.meta.url), "utf8"); const readerEnv = (name) => { const match = new RegExp(`^${name} = "([A-Z_]+)"$`, "m").exec(READER); @@ -258,7 +256,7 @@ for (const [workflow, dispatchName] of [ ["pr-feedback-dispatch.yml", "Dispatch address-feedback"], ["pr-unblock-dispatch.yml", "Dispatch pr-unblock for conflicting, review-tier Dependabot, failing lanes App and Markdown-changing PRs"], ]) { - test(`${workflow}: the carry is fetched on the first FAIL-cap read only, names no carried listing, and every read shares one saved listing`, async () => { + test(`${workflow}: the carry is fetched on the first FAIL-cap read only and every read shares one saved listing`, async () => { const steps = dispatchSteps(workflow); assert.deepEqual( steps.filter((s) => s.block.includes(".base/.github/scripts/pr-merge/")).map((s) => s.name), @@ -269,11 +267,9 @@ for (const [workflow, dispatchName] of [ assert.ok(dispatch, dispatchName); const LISTING = readerEnv("LISTING_ENV"); - const CARRY = readerEnv("LISTING_CARRY_ENV"); const env = stepEnv(dispatch.block); assert.ok(env.VERIFY_EVIDENCE_SCRIPT, "the dispatch builds its FAIL-cap reader"); assert.equal(env.GITHUB_TOKEN, "${{ github.token }}", "the carry is fetched with the job's token"); - assert.equal(env[CARRY], undefined, "no gate saves a listing to carry"); const order = []; const read = verifyCapReader(env, { spawn: (command, args, options) => { @@ -335,24 +331,13 @@ test("the carry starts the reader's run cache from pr-merge's file, shares its G assert.equal(readFileSync(output, "utf8"), ""); }); -test("the carry never fetches pr-merge's artifact listing, and a job that names only the head-ref map fetches only the map", () => { - assert.deepEqual(carrySteps({ RUNNER_TEMP: "/t", BABYSIT_VERIFY_LISTING_CARRY: "/t/verify-listing-carry.json" }), []); +test("a job that names only the head-ref map fetches only the map", () => { assert.deepEqual( carrySteps({ RUNNER_TEMP: "/t", BABYSIT_VERIFY_HEAD_REFS: "/t/head-refs-in.json" }).map(({ script, args }) => [script, ...args]), [["head-ref-map.mjs", "read"]], ); }); -test("a job that names only the listing carry fetches no carry", async () => { - let fetched = 0; - const read = verifyCapReader( - { GITHUB_REPOSITORY: REPO, BABYSIT_VERIFY_LISTING_CARRY: "/runner/temp/verify-listing-carry.json" }, - { spawn: () => answer({ refusal: null }), carry: () => (fetched += 1), log: () => {} }, - ); - assert.equal(await read({ number: 42, head: HEAD, base: BASE }), null); - assert.equal(fetched, 0); -}); - test("a carry that fails is fetched once, warns, and the reads go on without it", async () => { let fetched = 0; const lines = []; 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 6e642ac36a5..855b5c7b50b 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 @@ -164,8 +164,7 @@ (a carried record counts the FAIL when it shows origin; any other answer from it could turn the FAIL away, so it is read again), with one exception: the artifact listing. A listing begun at most `LISTING_CARRY_MAX_AGE` ago (this -Reader's own, the run's saved one, or one a trusted earlier run saved that -`LISTING_CARRY_ENV` names, which no workflow sets; index mode saves a listing +Reader's own or the run's saved one; index mode saves a listing only past `INDEX_NAMES_CAP`) is a base: `Reader._incremental` reads only the newest pages, newest id first, down past the base's highest id until an artifact created `LISTING_OVERLAP` before the base was read, and keeps the base's entries below @@ -304,7 +303,6 @@ TRUSTED_ACTORS_PATH = ".github/standards/trusted-actors/trusted-actors.json" TOKEN_ENV = "BABYSIT_VERIFY_GH_TOKEN" LISTING_ENV = "BABYSIT_VERIFY_LISTING" -LISTING_CARRY_ENV = "BABYSIT_VERIFY_LISTING_CARRY" # `1`: no wait between incremental reads (`no_incremental_waits`). NO_INCREMENTAL_WAITS_ENV = "BABYSIT_VERIFY_NO_INCREMENTAL_WAITS" HEAD_REFS_ENV = "BABYSIT_VERIFY_HEAD_REFS" @@ -436,8 +434,7 @@ def verdict_name(name: str) -> re.Match[str] | None: LISTING_OVERLAP = timedelta(hours=2) # The oldest base an incremental listing starts from. Scheduled pr-merge runs # come four times an hour (pr-merge.yml `cron`), and rotation.mjs asks at most -# 10 trusted runs (`MAX_RUNS`), about 2.5 hours of them, for the carried -# listing. At the most uploads seen in an hour on 2026-10-27 (255), a base this +# 10 trusted runs (`MAX_RUNS`), about 2.5 hours of them. At the most uploads seen in an hour on 2026-10-27 (255), a base this # old and the overlap page at most about 1,300 artifacts, 13 pages, against 69 # for the whole listing that day (judgment on that data). LISTING_CARRY_MAX_AGE = timedelta(hours=3) @@ -549,6 +546,8 @@ def _overcount_refusal(outcome: str) -> str: if outcome.startswith(DEADLINE_UNREADABLE): return f"{DEADLINE_UNREADABLE}: {refusal} ({outcome})" return refusal + + # The whole listings `Reader._complete_listing` reads before it refuses. Of # three whole listings read on 2026-10-27, two skipped live artifacts at page # boundaries (1 and 3, a FAIL in each), and each skipped artifact was in every @@ -810,7 +809,9 @@ def task_lines(item: dict[str, Any]) -> list[str]: (int(tag[1]), tag[2]) if tag and int(float(tag[1])) == int(tag[1]) else None ) scoped = rows[-1].startswith(SCOPE_PREFIX) - header = f"{split_child_header(number, int(part[1]), int(part[2]), source, scoped)}\n" + header = ( + f"{split_child_header(number, int(part[1]), int(part[2]), source, scoped)}\n" + ) if not body.startswith(header): return None refinement: list[str] = [] @@ -1287,12 +1288,6 @@ class Reader: listing_path: str | None = field( default_factory=lambda: os.environ.get(LISTING_ENV) or None ) - # A listing an earlier trusted run saved (`LISTING_CARRY_ENV`), as a base - # for `_incremental`. No workflow names one: in index mode no gate saves - # a listing to carry. - carry_path: str | None = field( - default_factory=lambda: os.environ.get(LISTING_CARRY_ENV) or None - ) # The head-ref map (`HEAD_REFS_ENV`), when pr-merge.yml names one. head_refs_path: str | None = field( default_factory=lambda: os.environ.get(HEAD_REFS_ENV) or None @@ -1391,10 +1386,12 @@ class Reader: # FAIL index by exact name instead of the whole listing. `LEGACY_FAILS_ENV` # names another file, or none when empty. legacy_path: str | None = field( - default_factory=lambda: os.environ.get( - LEGACY_FAILS_ENV, str(Path(__file__).with_name(LEGACY_FAILS_FILE)) + default_factory=lambda: ( + os.environ.get( + LEGACY_FAILS_ENV, str(Path(__file__).with_name(LEGACY_FAILS_FILE)) + ) + or None ) - or None ) _legacy_entries: list[dict[str, Any]] | None = None _legacy_loaded: bool = False @@ -1404,9 +1401,7 @@ class Reader: _live_issues: dict[int, dict[Any, Any]] = field(default_factory=dict) # A targeted listing read for the answer key, taken by the next judgment # of the same pull request and head (`_verdict_artifacts`). - _handoff: dict[tuple[int, str], list[dict[str, Any]]] = field( - default_factory=dict - ) + _handoff: dict[tuple[int, str], list[dict[str, Any]]] = field(default_factory=dict) # The exact-name and run-artifact reads this Reader made. index_reads: int = 0 @@ -1530,14 +1525,13 @@ def _load_saved( def _incremental_base(self) -> tuple[datetime, list[dict[str, Any]]] | None: """The newest whole listing begun at most `LISTING_CARRY_MAX_AGE` ago - whose every entry `_listed_entry` takes: this Reader's own, the run's - saved one, or the one a trusted run carried (`carry_path`).""" + whose every entry `_listed_entry` takes: this Reader's own or the + run's saved one.""" bases = [ base for base in ( self._base, self._load_saved(self.listing_path, LISTING_CARRY_MAX_AGE), - self._load_saved(self.carry_path, LISTING_CARRY_MAX_AGE), ) if base is not None and _recent(base[0], LISTING_CARRY_MAX_AGE) @@ -2739,7 +2733,9 @@ def _legacy(self) -> list[dict[str, Any]] | None: or entry.get("origin") is not True or not isinstance(entry.get("name"), str) or not _is_id(run_id, run_id) - or not (entry.get("head_ref") is None or isinstance(entry["head_ref"], str)) + or not ( + entry.get("head_ref") is None or isinstance(entry["head_ref"], str) + ) ): return None for entry in entries: @@ -2791,7 +2787,9 @@ def _named(self, name: str, deadline: float) -> list[dict[str, Any]]: if len(items) < LISTING_PAGE_SIZE or page * LISTING_PAGE_SIZE >= count: break if page >= LISTING_TOP_UP_PAGES: - raise _Unreadable(f"the artifacts named {name} run past {page} pages") + raise _Unreadable( + f"the artifacts named {name} run past {page} pages" + ) page += 1 if totals == {len(found)}: return [a for a in found.values() if a["expired"] is False] @@ -2818,7 +2816,8 @@ def read() -> list[dict[str, Any]]: or answer[0].get("total_count") != len(items) or not all(map(_listed_entry, items)) or any( - json_object(a.get("workflow_run")).get("id") != run_id for a in items + json_object(a.get("workflow_run")).get("id") != run_id + for a in items ) ): raise _Unreadable(f"the artifacts of run {run_id} could not be read") @@ -2833,11 +2832,15 @@ def read() -> list[dict[str, Any]]: ) self.index_reads += 1 if record.get("status") != "completed": - raise _Unreadable(f"run {run_id} holds a FAIL index and is still running") + raise _Unreadable( + f"run {run_id} holds a FAIL index and is still running" + ) items = read() return items - def _index_hits(self, names: Iterable[str], deadline: float) -> list[dict[str, Any]]: + def _index_hits( + self, names: Iterable[str], deadline: float + ) -> list[dict[str, Any]]: """The artifacts, but the index ones, of every run that uploaded an artifact named one of `names`. An index from any run only points: each FAIL it leads to is judged by its own run, as in the listing.""" @@ -2848,7 +2851,9 @@ def _index_hits(self, names: Iterable[str], deadline: float) -> list[dict[str, A if _is_id(run_id, run_id): runs.add(run_id) if len(runs) > INDEX_RUNS_CAP: - raise _Unreadable(f"the FAIL index names {len(runs)} runs, over {INDEX_RUNS_CAP}") + raise _Unreadable( + f"the FAIL index names {len(runs)} runs, over {INDEX_RUNS_CAP}" + ) return [ a for run_id in sorted(runs) @@ -2924,7 +2929,11 @@ def targeted(self, number: int, head: str) -> list[dict[str, Any]]: listing = self._targeted( number, head, head_ref, self._commits(number, head) | {head} ) - except (_Unreadable, merge_policy.PolicyReadError, postmerge.PostMergeReadError) as exc: + except ( + _Unreadable, + merge_policy.PolicyReadError, + postmerge.PostMergeReadError, + ) as exc: raise RuntimeError(str(exc)) from exc self._handoff[(number, head)] = listing return listing @@ -2940,7 +2949,8 @@ def own_verdicts(self, number: int, head: str) -> list[dict[str, Any]]: found.update( (a["id"], a) for a in self._index_hits( - [f"{INDEX_PREFIX}pr-{number}", f"{INDEX_PREFIX}sha-{head}"], deadline + [f"{INDEX_PREFIX}pr-{number}", f"{INDEX_PREFIX}sha-{head}"], + deadline, ) ) failed = { @@ -3505,7 +3515,9 @@ def drafted(pr_n: int, sha: str) -> bool: raise _Unreadable(f"#{other} state: {exc}") from exc user = json_object(pull.get("user")) self._counted[other] = self._counts( - "MERGED" if pull.get("merged_at") else str(pull.get("state")).upper(), + "MERGED" + if pull.get("merged_at") + else str(pull.get("state")).upper(), { "__typename": user.get("type"), "login": _bot_login(str(user.get("login") or "")), @@ -3983,7 +3995,11 @@ def _queue_reads(self, pr: dict[str, Any]) -> bool: app_id = self._lane_app_id() except Exception: # noqa: BLE001 -- any failure keeps the hold return False - return app_id is not None and user.get("id") == app_id and user.get("type") == "Bot" + return ( + app_id is not None + and user.get("id") == app_id + and user.get("type") == "Bot" + ) def lane_risk_refusal( @@ -4782,8 +4798,8 @@ def idle_tick(refusal: str | None) -> str | None: base_sha=config.base_sha, identity=config.agent_identity, code_unchanged=True, - runs_unchanged=lambda answer, branch: ( - postmerge.pending_runs_unchanged(repo, answer, branch) + runs_unchanged=lambda answer, branch: postmerge.pending_runs_unchanged( + repo, answer, branch ), outcome=outcome, ) @@ -5032,11 +5048,11 @@ def _spent(comments: list[dict[str, Any]], identity: str, number: int) -> int: attempt-less record spends nothing.""" named = set() for c in comments: - if str( - json_object(c.get("user")).get("login") or "" - ).lower() == identity.lower() and ( - pr := _started_attempt(c.get("body")) - ) is not None: + if ( + str(json_object(c.get("user")).get("login") or "").lower() + == identity.lower() + and (pr := _started_attempt(c.get("body"))) is not None + ): named.add(pr) return len(named - {number}) @@ -5185,9 +5201,7 @@ def capped_close( # toggles only when no lanes App removal followed the record, and puts # agent-ready back only when the newest removal is that one # (`_lane_removals`). - plan: list[ - tuple[int, list[dict[str, Any]], tuple[int, int] | None, bool, str] - ] = [] + plan: list[tuple[int, list[dict[str, Any]], tuple[int, int] | None, bool, str]] = [] for issue in issues: comments = postmerge._paginated( f"repos/{repo}/issues/{issue}/comments?per_page=100", f"#{issue} comments" @@ -5473,14 +5487,21 @@ def open_window(self) -> None: if self.limited: raise self._stop(f"rate limited: {self.limited}") try: - core = json_object(json_object(json.loads(proc.stdout)).get("resources")).get("core") - remaining, reset = json_object(core)["remaining"], json_object(core)["reset"] + core = json_object( + json_object(json.loads(proc.stdout)).get("resources") + ).get("core") + remaining, reset = ( + json_object(core)["remaining"], + json_object(core)["reset"], + ) except (ValueError, KeyError, TypeError) as exc: raise self._stop(f"the rate limit could not be read: {exc}") from exc if type(remaining) is not int or type(reset) is not int: raise self._stop("the rate limit could not be read") if remaining < self.min_remaining: - raise self._stop(f"core remaining {remaining} is under {self.min_remaining}") + raise self._stop( + f"core remaining {remaining} is under {self.min_remaining}" + ) if self.window_reset is not None: self.report() kept = 0 @@ -5537,7 +5558,9 @@ def _kept_run(run: dict[str, Any]) -> dict[str, Any]: """The fields of a run record the build keeps: its status and origin.""" return { **{k: run.get(k) for k in RUN_FIELDS_KEPT}, - "repository": {"full_name": json_object(run.get("repository")).get("full_name")}, + "repository": { + "full_name": json_object(run.get("repository")).get("full_name") + }, "head_repository": { "full_name": json_object(run.get("head_repository")).get("full_name") }, @@ -5551,9 +5574,13 @@ def cutover_of(reader: Reader, merge_sha: str) -> datetime: if merge_policy.SHA_RE.fullmatch(merge_sha) is None: raise _Unreadable("--merge-sha must be a full commit SHA") commit = json_object( - reader._actions(["api", f"repos/{reader.repo}/commits/{merge_sha}"], "merge commit") + reader._actions( + ["api", f"repos/{reader.repo}/commits/{merge_sha}"], "merge commit" + ) + ) + date = _when( + json_object(json_object(commit.get("commit")).get("committer")).get("date") ) - date = _when(json_object(json_object(commit.get("commit")).get("committer")).get("date")) if date is None: raise _Unreadable(f"commit {merge_sha} has no committer date") if not reader._contains(merge_sha): @@ -5610,7 +5637,9 @@ def load(self) -> None: "runs": {}, } try: - saved = json_object(json.loads(self.checkpoint_path.read_text(encoding="utf-8"))) + saved = json_object( + json.loads(self.checkpoint_path.read_text(encoding="utf-8")) + ) except FileNotFoundError: self.state = fresh return @@ -5620,7 +5649,9 @@ def load(self) -> None: saved.get(k) != fresh[k] for k in ("version", "repo", "since", "cutover", "lookback") ): - raise _Unreadable("the checkpoint is for another repository, range or cutover") + raise _Unreadable( + "the checkpoint is for another repository, range or cutover" + ) self.state = saved def save(self) -> None: @@ -5634,7 +5665,9 @@ def _window( """Every verifier run created in [start, end), or [start, end] for the `last` window, so the cutover's own second is read: each page holds the same `total_count`, and the ids, each once, equal it.""" - created = f"{_stamp(start)}..{_stamp(end if last else end - timedelta(seconds=1))}" + created = ( + f"{_stamp(start)}..{_stamp(end if last else end - timedelta(seconds=1))}" + ) found: dict[int, dict[str, Any]] = {} totals: set[Any] = set() page = 1 @@ -5656,7 +5689,9 @@ def _window( middle = split_created_window(start, end, RUN_WINDOW_MINIMUM) if middle is None: raise _Unreadable(f"{count} verifier runs created {created}") - return self._window(start, middle) + self._window(middle, end, last=last) + return self._window(start, middle) + self._window( + middle, end, last=last + ) totals.add(count) for run in map(json_object, runs): if not _is_id(run.get("id"), run.get("id")): @@ -5665,7 +5700,10 @@ def _window( # Read on while the page is full, never stopping at the count a # page claims, which can be low; the totals check below fails a # count the runs do not equal. - if len(runs) < LISTING_PAGE_SIZE or page >= RUN_SEARCH_CAP // LISTING_PAGE_SIZE: + if ( + len(runs) < LISTING_PAGE_SIZE + or page >= RUN_SEARCH_CAP // LISTING_PAGE_SIZE + ): break page += 1 if totals != {len(found)}: @@ -5676,7 +5714,9 @@ def _window( # again once the run has completed. for record in found.values(): if record.get("status") != "completed": - raise _Unreadable(f"verifier run {record['id']} is {record.get('status')}") + raise _Unreadable( + f"verifier run {record['id']} is {record.get('status')}" + ) return list(found.values()) def _run(self, record: dict[str, Any]) -> dict[str, Any]: @@ -5705,7 +5745,9 @@ def _run(self, record: dict[str, Any]) -> dict[str, Any]: or count != len(items) or count >= LISTING_PAGE_SIZE or not all(map(_listed_entry, items)) - or any(json_object(a.get("workflow_run")).get("id") != run_id for a in items) + or any( + json_object(a.get("workflow_run")).get("id") != run_id for a in items + ) ): raise _Unreadable(f"the artifacts of run {run_id} do not hold their count") names = [str(a.get("name") or "") for a in items] @@ -5724,19 +5766,22 @@ def _run(self, record: dict[str, Any]) -> dict[str, Any]: raise _Unreadable(f"the origin of run {run_id}: {exc}") from exc if not origin: return {"why": "foreign", "entries": []} - return {"why": "kept", "entries": [ - { - "id": a["id"], - "name": a["name"], - "created_at": a.get("created_at"), - "expires_at": a.get("expires_at"), - "expired": False, - "workflow_run": {"id": run_id}, - "origin": True, - "head_ref": self.reader._head_ref(run_id, a["name"], a["id"], ""), - } - for a in fails - ]} + return { + "why": "kept", + "entries": [ + { + "id": a["id"], + "name": a["name"], + "created_at": a.get("created_at"), + "expires_at": a.get("expires_at"), + "expired": False, + "workflow_run": {"id": run_id}, + "origin": True, + "head_ref": self.reader._head_ref(run_id, a["name"], a["id"], ""), + } + for a in fails + ], + } def _listing_fails(self) -> list[dict[str, Any]]: """Every unexpired FAIL-shaped artifact one whole listing shows, by @@ -5954,7 +5999,10 @@ def _legacy_fails_main(args: argparse.Namespace, now: datetime | None) -> int: since = _when(args.since) asked = _when(args.cutover) if args.cutover else None if since is None or (args.cutover and asked is None): - print("legacy-fails: --since and --cutover must be times with a zone", file=sys.stderr) + print( + "legacy-fails: --since and --cutover must be times with a zone", + file=sys.stderr, + ) return 2 pacer = _Pacer( postmerge.gh_capture, @@ -5985,7 +6033,10 @@ def _legacy_fails_main(args: argparse.Namespace, now: datetime | None) -> int: ) except _BuildStopped as exc: pacer.report() - print(f"legacy-fails: stopped, resume from {args.checkpoint}: {exc}", file=sys.stderr) + print( + f"legacy-fails: stopped, resume from {args.checkpoint}: {exc}", + file=sys.stderr, + ) return 3 except _Unreadable as exc: pacer.report() diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_verify_evidence.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_verify_evidence.py index 74407a1082d..d05cff608f5 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_verify_evidence.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_verify_evidence.py @@ -2828,9 +2828,7 @@ def test_a_closing_pass_that_credited_siblings_counts_as_any_closing_one( fake.body = "Closes #7\n\nSiblings: #12917, #12937" fake.records[named(HEAD, "pass")] = record(spec_kind="closes") self.assertIsNone(read(fake).refusal) - self.assertFalse( - any("12917" in " ".join(c) for c in fake.calls), fake.calls - ) + self.assertFalse(any("12917" in " ".join(c) for c in fake.calls), fake.calls) fake = passing() fake.body = "Closes #7\n\nSiblings: #12917, #12937" fake.closing_edited = {7: "2026-10-15T00:00:01Z"} @@ -2935,7 +2933,11 @@ def test_only_the_lanes_apps_entry_is_left_to_the_queue(self) -> None: cases: dict[str, tuple[dict[str, Any], Any, str]] = { "lanes App": ({"id": self.APP, "type": "Bot"}, self.APP, ve.QUEUE_READS), "owner": ({"id": 7, "type": "User"}, self.APP, ve.UNREADABLE), - "same id, not a Bot": ({"id": self.APP, "type": "User"}, self.APP, ve.UNREADABLE), + "same id, not a Bot": ( + {"id": self.APP, "type": "User"}, + self.APP, + ve.UNREADABLE, + ), "other App": ({"id": 99, "type": "Bot"}, self.APP, ve.UNREADABLE), "no App id": ({"id": self.APP, "type": "Bot"}, None, ve.UNREADABLE), "App id unreadable": ( @@ -4150,8 +4152,8 @@ def verify_cap_env(reads: int = 1) -> dict[str, str]: return {k: v for k, v in env.items() if k.startswith("BABYSIT_VERIFY_")} -class TheListingBuildsOnACarriedOne(unittest.TestCase): - """A listing a trusted pr-merge run saved (`BABYSIT_VERIFY_LISTING_CARRY`) +class TheListingBuildsOnASavedOne(unittest.TestCase): + """A listing saved for the run (`BABYSIT_VERIFY_LISTING`) is a base: the reader pages only down past it, keeps the result only when it holds the first page's live `total_count`, and lists every page on any doubt.""" @@ -4159,14 +4161,12 @@ class TheListingBuildsOnACarriedOne(unittest.TestCase): def setUp(self) -> None: tmp = tempfile.TemporaryDirectory() self.addCleanup(tmp.cleanup) - self.carry = pathlib.Path(tmp.name, "verify-listing-carry.json") self.saved = pathlib.Path(tmp.name, "verify-listing.json") self.failed = pathlib.Path(f"{self.saved}{ve.FAILED_LISTING_SUFFIX}") for patcher in ( mock.patch.dict( "os.environ", { - "BABYSIT_VERIFY_LISTING_CARRY": str(self.carry), "BABYSIT_VERIFY_LISTING": str(self.saved), }, ), @@ -4189,8 +4189,8 @@ def setUp(self) -> None: dated("pr-merge-gate-cursor", 3, 700, old), ] - def write_carry(self, artifacts: list[Any], at: datetime | None = None) -> None: - self.carry.write_text( + def write_saved(self, artifacts: list[Any], at: datetime | None = None) -> None: + self.saved.write_text( json.dumps( {"fetchedAt": (at or self.base_at).isoformat(), "artifacts": artifacts} ) @@ -4216,7 +4216,7 @@ def page(args: list[str], **kwargs: Any) -> Any: return reader, reader.evidence(PR, HEAD, pull()) def test_a_fail_uploaded_since_is_read_from_the_first_page_alone(self) -> None: - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake(dated(named(HEAD, "fail"), 2, 1100, LISTED_AT)) reader, evidence = self.judge(fake) self.assertEqual(evidence.refusal, "verify:fail") @@ -4240,7 +4240,7 @@ def test_the_overlap_reads_a_lower_id_created_after_the_base(self) -> None: dated("pr-merge-gate-cursor", 3, 990, self.base_at - timedelta(minutes=30)), *self.base[1:], ] - self.write_carry(base) + self.write_saved(base) fake = self.fake(dated(named(HEAD, "fail"), 2, 950, LISTED_AT), base=base) reader, evidence = self.judge(fake) self.assertEqual(evidence.refusal, "verify:fail") @@ -4259,7 +4259,7 @@ def test_an_expired_carried_entry_is_dropped_by_githubs_date(self) -> None: expires=LISTED_AT + timedelta(seconds=2), ) gone = dated("pr-merge-gate-cursor", 3, 700, self.base_at, expired=True) - self.write_carry([*self.base[:2], lapsed, gone]) + self.write_saved([*self.base[:2], lapsed, gone]) fake = self.fake(base=self.base[:2]) reader, evidence = self.judge(fake) self.assertIsNone(evidence.refusal) @@ -4270,7 +4270,7 @@ def test_an_expired_carried_entry_is_dropped_by_githubs_date(self) -> None: def test_a_carried_entry_the_pages_no_longer_hold_is_dropped(self) -> None: # An overwrite deletes 1000's artifact and uploads 1050 in its place. - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake( dated(named(HEAD, "pass"), 1, 1050, LISTED_AT), base=self.base[1:] ) @@ -4309,7 +4309,7 @@ def test_a_carried_entry_deleted_before_its_expiry_is_read_again(self) -> None: # so first page and base hold one more than the live count. The pages # around the entries near expiry decide them instead of a full listing. base = self.tail((500, 30), (400, 40)) - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] != 500]) reader, evidence = self.judge(fake) self.assertIsNone(evidence.refusal) @@ -4325,7 +4325,7 @@ def test_an_early_deletion_out_of_expiry_order_is_read_again(self) -> None: # 400 expires before 500 but GitHub deleted only 400: the pages, not # the expiry order, decide which entry went. base = self.tail((500, 30), (400, 20)) - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] != 400]) reader, _ = self.judge(fake) self.assertEqual(reader.listing_mode, "incremental") @@ -4341,7 +4341,7 @@ def test_a_fail_a_forged_carry_drops_beside_an_early_deletion_counts( # A forged carry leaves out the FAIL 450 GitHub lists between 500 and # 400, both deleted early: the region's pages bring the FAIL back. base = self.tail((500, 30), (400, 40)) - self.write_carry(base) + self.write_saved(base) fail = dated(named(EARLIER, "fail"), 2, 450, self.base_at - timedelta(days=2)) live = [a for a in base if a["id"] not in (500, 400)] fake = self.fake(fail, base=live) @@ -4359,7 +4359,7 @@ def test_a_failed_read_of_the_pages_near_expiry_lists_every_page(self) -> None: # The top-up's count is exact, but 400 is near expiry, so it reads # page 3 too and refuses as transient: nothing noted for the job. base = self.tail((500, 30), (400, 40)) - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] != 500]) def page(args: list[str], **kwargs: Any) -> Any: @@ -4386,7 +4386,7 @@ def test_a_deletion_far_from_expiry_still_lists_every_page(self) -> None: # 300 expires in 90 days but GitHub no longer lists it, and nothing # in the base is near expiry: no region to read, so every page. base = self.tail() - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] != 300]) reader, _ = self.judge(fake) self.assertEqual(reader.listing_mode, "full") @@ -4421,7 +4421,7 @@ def test_a_deletion_between_region_pages_keeps_the_fail_it_shifts( fail, *(dated("pr-merge-gate-cursor", 3, i, old) for i in (400, 300)), ] - self.write_carry(carry) + self.write_saved(carry) fake = self.fake(base=[a for a in carry if a["id"] not in (800, 500)]) shifted = [False] @@ -4462,7 +4462,7 @@ def test_an_id_the_region_pages_omit_is_dropped_only_on_a_404(self) -> None: missing_ok=True ) base = self.tail((500, 30), (400, 40)) - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] != 500]) fake.fail = fail blipped = [label != "502-404"] @@ -4495,8 +4495,10 @@ def climbing(self, missing: int) -> tuple[ve.Reader, FakeGitHub]: re-read starts below 500's page and walks up.""" old = self.base_at - timedelta(hours=3) near = LISTED_AT + timedelta(minutes=30) - phantoms = (850, 750, 650, 550, 450) if missing == 4 else ( - 875, 850, 825, 775, 750, 650, 550 + phantoms = ( + (850, 750, 650, 550, 450) + if missing == 4 + else (875, 850, 825, 775, 750, 650, 550) ) held = [ *self.base, @@ -4504,7 +4506,7 @@ def climbing(self, missing: int) -> tuple[ve.Reader, FakeGitHub]: dated("pr-merge-gate-cursor", 3, 500, old, expires=near), *(dated("pr-merge-gate-cursor", 3, i, old) for i in (400, 300)), ] - self.write_carry( + self.write_saved( [*held, *(dated("pr-merge-gate-cursor", 3, i, old) for i in phantoms)] ) below = [250, 240, 230, 220, 210, 200][:missing] @@ -4538,7 +4540,7 @@ def test_the_pages_near_expiry_stop_at_their_cap(self) -> None: # keeps 500, near expiry, and reads its page under a cap of its own, # which the incremental read did not spend: it settles and allows. base = self.tail((500, 30), (400, 20)) - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] != 400]) with mock.patch.object(ve, "NEAR_EXPIRY_PAGE_CAP", 1): reader, evidence = self.judge(fake) @@ -4580,7 +4582,7 @@ def early_deletion_and_a_new_fail(self) -> FakeGitHub: page read skips once: the first read holds the live count, 500's own read answers 404, and it is one short.""" base = self.tail((500, 30)) - self.write_carry(base) + self.write_saved(base) fail = dated(named(HEAD, "fail"), 2, 1100, LISTED_AT) fake = self.fake(fail, base=[a for a in base if a["id"] != 500]) self.skips_once(fake, 1100) @@ -4611,7 +4613,7 @@ def test_an_over_count_left_short_by_a_skip_is_read_again_after_a_wait( # skipped. Read again after one wait, it holds the count: no whole # listing, so the job's over-count slot and failure note stay unused. base = self.tail((500, 30), (400, 40)) - self.write_carry(base) + self.write_saved(base) fail = dated(named(HEAD, "fail"), 2, 1100, LISTED_AT) fake = self.fake(fail, base=[a for a in base if a["id"] not in (500, 400)]) self.skips_once(fake, 1100) @@ -4680,14 +4682,14 @@ def test_two_readers_that_over_count_take_one_whole_listing_a_job( # GitHub dropped 300, 90 days from expiry, so both Readers' pages over # the carry hold one more than the live count. The first takes the # whole listing, which holds the count, and allows. The second, its - # saved listing gone (as past its age), refuses without another. + # saved listing back at the old base, refuses without another. base = self.tail() - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] != 300]) _, first = self.judge(fake) self.assertIsNone(first.refusal, first.detail) self.assertEqual(fake.full_listings(), 1) - self.saved.unlink() + self.write_saved(base) reader, second = self.judge(fake) self.assertEqual(second.refusal, "verify:unreadable") self.assertIn("already took its whole listing", second.detail or "") @@ -4703,7 +4705,7 @@ def test_an_over_count_refused_before_paging_leaves_the_job_its_listing( # verify-cap's limit, and its whole listing does not fit, so it # refuses before paging. PR B's Reader, with the time, takes it. base = self.tail() - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] != 300]) first = self.against_the_clock(fake, 0.0, limit="20") self.assertEqual(first.refusal, "verify:unreadable") @@ -4720,7 +4722,7 @@ def test_an_over_count_listing_out_of_the_callers_time_refuses_so_later( # time. B's Reader takes no second whole listing, and its refusal # keeps the caller-limit prefix, so verify-cap skips B's round too. base = self.tail() - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] != 300]) first = self.against_the_clock(fake, 50.0, limit="60") self.assertTrue(first.detail.startswith(ve.DEADLINE_UNREADABLE)) @@ -4740,7 +4742,7 @@ def test_an_over_count_the_pages_near_expiry_leave_takes_one_whole_listing( # another whole listing would restore 700, but an over-count takes # one a job, and the refusal is noted for the job's later Readers. base = self.tail((500, 30)) - self.write_carry(base) + self.write_saved(base) fake = self.fake(base=[a for a in base if a["id"] not in (500, 300)]) fake.full_skips = [{700}] reader, evidence = self.judge(fake) @@ -4759,7 +4761,7 @@ def test_a_count_other_than_githubs_lists_every_page(self) -> None: # A forged carry drops an old FAIL (600): the live count holds it, so # the merged listing is one short and every page is read. fail = dated(named(EARLIER, "fail"), 2, 600, self.base_at - timedelta(days=2)) - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake(fail) reader, evidence = self.judge(fake) self.assertEqual(evidence.refusal, "verify:earlier-fail") @@ -4776,7 +4778,7 @@ def test_a_caller_that_skips_the_waits_lists_every_page_after_one_miss( # incremental first page, no wait, then the whole listing, which still # holds the dropped FAIL. fail = dated(named(EARLIER, "fail"), 2, 600, self.base_at - timedelta(days=2)) - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake(fail) with mock.patch.dict( "os.environ", {"BABYSIT_VERIFY_NO_INCREMENTAL_WAITS": "1"} @@ -4786,8 +4788,8 @@ def test_a_caller_that_skips_the_waits_lists_every_page_after_one_miss( self.assertEqual(reader.listing_mode, "full") self.assertEqual((fake.pages(), fake.full_listings()), (1 + 1, 1)) self.sleep.assert_not_called() - # Any other value keeps the waits (with no listing saved by the first). - self.saved.unlink() + # Any other value keeps the waits (the base saved again). + self.write_saved(self.base) self.sleep.reset_mock() with mock.patch.dict( "os.environ", {"BABYSIT_VERIFY_NO_INCREMENTAL_WAITS": "true"} @@ -4802,7 +4804,7 @@ def verify_cap_read( whose pages miss GitHub's live count, a count no whole listing fits in the 105 s its 120 s kill limit leaves. The miss outlasts the waits unless a test ends it.""" - self.write_carry(self.base) + self.write_saved(self.base) fake = self.miss = self.fake() fake.live_total = 12_000 with ( @@ -4852,7 +4854,7 @@ def test_a_failed_first_read_is_not_read_again_on_the_base(self) -> None: for label, failure in failures.items(): with self.subTest(label): self.saved.unlink(missing_ok=True) - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake(dated(named(HEAD, "fail"), 2, 1100, LISTED_AT)) first = [True] @@ -4882,7 +4884,7 @@ def cut_short( self, times: int, failure: Any ) -> tuple[ve.Reader, ve.Evidence, FakeGitHub]: """The first incremental page fails with `failure` `times` times.""" - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake(dated(named(HEAD, "fail"), 2, 1100, LISTED_AT)) left = [times] @@ -4937,7 +4939,7 @@ def test_a_count_off_once_is_read_again_on_the_same_base(self) -> None: # The first read's live count is one more than its pages hold, as # when a page boundary moves during the read; the next read holds it # and no page is listed whole. - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake(dated(named(HEAD, "fail"), 2, 1100, LISTED_AT)) rest, off = fake.rest, [len(self.base) + 2] @@ -4955,7 +4957,7 @@ def first_read_off(args: list[str], *rest_args: Any) -> Any: def misses_then_reads(self, misses: int) -> tuple[FakeGitHub, str]: """GitHub's live count one more than the pages hold on the first `misses` incremental reads, then right; the judge's stderr.""" - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake(dated(named(HEAD, "fail"), 2, 1100, LISTED_AT)) rest, off = fake.rest, [len(self.base) + 2] * misses @@ -5018,7 +5020,7 @@ def test_no_wait_runs_past_the_reads_timeout(self) -> None: with mock.patch.object( ve, "_listing_clock", side_effect=lambda: next(clock, 300.0) ): - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake(dated(named(HEAD, "fail"), 2, 1100, LISTED_AT)) fake.live_total = len(self.base) + 2 reader, _ = self.judge(fake) @@ -5030,17 +5032,17 @@ def test_a_live_count_no_listing_can_hold_refuses(self) -> None: # an entry GitHub no longer counts, which no further listing drops. for delta, reads in ((-1, 1), (1, ve.LISTING_READS)): with self.subTest(delta): - self.write_carry(self.base) - self.saved.unlink(missing_ok=True) + self.write_saved(self.base) self.failed.unlink(missing_ok=True) fake = self.fake() fake.live_total = len(self.base) + delta + base_text = self.saved.read_text() reader, evidence = self.judge(fake) self.assertEqual(evidence.refusal, "verify:unreadable") self.assertIn("live artifact count", evidence.detail or "") self.assertEqual(reader.listing_mode, "full") self.assertEqual(fake.full_listings(), reads) - self.assertFalse(self.saved.exists()) + self.assertEqual(self.saved.read_text(), base_text) def skewed(self, lead: timedelta = timedelta(minutes=30)) -> FakeGitHub: """GitHub as on 2026-11-01 08:00-09:00Z: its count drops artifacts up @@ -5056,7 +5058,7 @@ def skewed(self, lead: timedelta = timedelta(minutes=30)) -> FakeGitHub: ] dropping = dated("pr-merge-gate-cursor", 3, 650, old, expires=soon) ahead = dated("pr-merge-gate-cursor", 3, 1100, LISTED_AT + timedelta(hours=1)) - self.write_carry([*self.base, *dropped]) + self.write_saved([*self.base, *dropped]) fake = self.fake(ahead, dropping) listed = fake.listed @@ -5088,13 +5090,14 @@ def test_a_count_the_pages_near_expiry_miss_takes_one_whole_listing_a_job( # Dropped three hours before expiry, past EARLY_DELETION_LEAD: no page # near expiry is read, and the over-count goes to one whole listing. fake = self.skewed(timedelta(hours=3)) + base_text = self.saved.read_text() reader, evidence = self.judge(fake) self.assertEqual(evidence.refusal, "verify:unreadable") self.assertIn("held more artifacts than", evidence.detail or "") self.assertEqual(reader.listing_mode, "full") # Another whole listing only adds ids, so it would hold 650 too. self.assertEqual(fake.full_listings(), 1) - self.assertFalse(self.saved.exists()) + self.assertEqual(self.saved.read_text(), base_text) self.assertIn("held more artifacts than", self.failed.read_text()) # The job's next Reader, in this process or another, pages nothing. listed = (fake.pages(), fake.full_listings()) @@ -5289,7 +5292,7 @@ def test_uploads_stamped_ahead_of_the_runner_still_read_incrementally( ) -> None: # After the skew: a FAIL and another upload stamped an hour ahead of # the runner's clock, and a count that holds. - self.write_carry(self.base) + self.write_saved(self.base) ahead = LISTED_AT + timedelta(hours=1) fake = self.fake( dated(named(HEAD, "fail"), 2, 1100, ahead), @@ -5308,8 +5311,7 @@ def test_a_carry_past_its_age_bound_lists_every_page(self) -> None: (timedelta(hours=3, seconds=1), "full"), ): with self.subTest(age): - self.write_carry(self.base, LISTED_AT - age) - self.saved.unlink(missing_ok=True) + self.write_saved(self.base, LISTED_AT - age) fake = self.fake() reader, _ = self.judge(fake) self.assertEqual(reader.listing_mode, mode) @@ -5332,8 +5334,7 @@ def test_a_carry_that_does_not_parse_lists_every_page(self) -> None: ), ): with self.subTest(text[:60]): - self.carry.write_text(text) - self.saved.unlink(missing_ok=True) + self.saved.write_text(text) fake = self.fake() reader, _ = self.judge(fake) self.assertEqual(reader.listing_mode, "full") @@ -5349,8 +5350,7 @@ def test_no_carry_lists_every_page(self) -> None: def test_a_failed_page_read_lists_every_page(self) -> None: for error in ("gh: Bad Gateway (HTTP 502)", "gh: Not Found (HTTP 404)"): with self.subTest(error): - self.write_carry(self.base) - self.saved.unlink(missing_ok=True) + self.write_saved(self.base) self.failed.unlink(missing_ok=True) fake = self.fake(dated(named(HEAD, "fail"), 2, 1100, LISTED_AT)) fake.page_fail = error @@ -5362,7 +5362,7 @@ def test_a_failed_page_read_lists_every_page(self) -> None: self.assertEqual(fake.full_listings(), 1) def test_a_page_with_no_date_or_an_odd_entry_lists_every_page(self) -> None: - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake() def undated(args: list[str], **kwargs: Any) -> Any: @@ -5409,7 +5409,7 @@ def test_an_undated_carried_entry_is_kept_below_the_new_pages(self) -> None: # The carry holds an undated FAIL below the pages read now: no date # prunes it, and the incremental listing still holds the live count. fail = self.undated(named(HEAD, "fail"), 2, 850) - self.write_carry([*self.base[:2], fail, *self.base[2:]]) + self.write_saved([*self.base[:2], fail, *self.base[2:]]) fake = self.fake(fail) reader, evidence = self.judge(fake) self.assertEqual(evidence.refusal, "verify:fail") @@ -5419,7 +5419,7 @@ def test_an_undated_carried_entry_is_kept_below_the_new_pages(self) -> None: def test_an_undated_upload_since_is_read_and_paging_stops_on_dated_ones( self, ) -> None: - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake(self.undated(named(HEAD, "fail"), 2, 1100)) reader, evidence = self.judge(fake) self.assertEqual(evidence.refusal, "verify:fail") @@ -5429,7 +5429,7 @@ def test_an_undated_upload_since_is_read_and_paging_stops_on_dated_ones( def test_a_refresh_builds_on_the_listing_it_drops(self) -> None: # The gate's re-list before an allow (babysit_merge.py `refresh`) # pages only past the run's own listing. - self.write_carry(self.base) + self.write_saved(self.base) fake = self.fake() page = lambda args, **kwargs: (fake(args, **kwargs), {"date": DATE}) # noqa: E731 with ( @@ -5545,10 +5545,10 @@ class AnExactCountChecksItsEntriesNearExpiry(unittest.TestCase): do not settle is refused, and a listing with none near expiry reads nothing more.""" - setUp = TheListingBuildsOnACarriedOne.setUp - write_carry = TheListingBuildsOnACarriedOne.write_carry - fake = TheListingBuildsOnACarriedOne.fake - judge = TheListingBuildsOnACarriedOne.judge + setUp = TheListingBuildsOnASavedOne.setUp + write_saved = TheListingBuildsOnASavedOne.write_saved + fake = TheListingBuildsOnASavedOne.fake + judge = TheListingBuildsOnASavedOne.judge def entry(self, artifact_id: int, expires: timedelta | None = None) -> Any: """An old gate cursor, expiring `expires` after the runner's clock.""" @@ -5613,7 +5613,7 @@ def test_a_stale_carry_whose_early_deletion_hides_a_missed_fail(self) -> None: # so the count is one short. A short count reads again after each # wait, as for a skip in the fresh pages; this one is the carry's, so # every read stays short and the whole listing holds the FAIL. - self.write_carry([*self.base, self.entry(600, timedelta(minutes=30))]) + self.write_saved([*self.base, self.entry(600, timedelta(minutes=30))]) fake = self.fake(self.earlier_fail(750)) reader, evidence = self.judge(fake) self.assertEqual(evidence.refusal, "verify:earlier-fail", evidence.detail) @@ -5645,7 +5645,7 @@ def test_a_dropped_entry_does_not_spend_the_top_ups_page_cap(self) -> None: # whole listing's top-up, which keeps 650, takes page 3 again under a # cap of its own, and settles. near = self.entry(650, timedelta(minutes=30)) - self.write_carry([*self.base, near, self.entry(600, timedelta(minutes=30))]) + self.write_saved([*self.base, near, self.entry(600, timedelta(minutes=30))]) fake = self.fake(self.earlier_fail(750), near) with mock.patch.object(ve, "NEAR_EXPIRY_PAGE_CAP", 6): reader, evidence = self.judge(fake) @@ -5661,7 +5661,7 @@ def test_a_short_count_reads_no_pages_near_expiry(self) -> None: # A short count is never kept, so it waits and reads again as before; # only the whole listing's exact top-up reads 600's page. near = self.entry(600, timedelta(minutes=30)) - self.write_carry([*self.base, near]) + self.write_saved([*self.base, near]) fake = self.fake(self.earlier_fail(750), near) reader, evidence = self.judge(fake) self.assertEqual(evidence.refusal, "verify:earlier-fail", evidence.detail) @@ -5673,7 +5673,7 @@ def test_a_short_count_reads_no_pages_near_expiry(self) -> None: def test_an_exact_count_with_none_near_expiry_reads_nothing_more(self) -> None: # 600 expires three hours on, past EARLY_DELETION_LEAD: page 1 alone. - self.write_carry([*self.base, self.entry(600, timedelta(hours=3))]) + self.write_saved([*self.base, self.entry(600, timedelta(hours=3))]) fake = self.fake(self.entry(600, timedelta(hours=3))) reader, evidence = self.judge(fake) self.assertIsNone(evidence.refusal, evidence.detail) @@ -5690,7 +5690,7 @@ def test_an_exact_count_the_pages_near_expiry_cannot_settle_refuses( # caps alone left it unsettled, so nothing is noted for the job and # the next Reader, its caps fresh, settles the count and allows. near = self.entry(600, timedelta(minutes=30)) - self.write_carry([*self.base, near]) + self.write_saved([*self.base, near]) fake = self.fake(near) stderr = io.StringIO() with ( @@ -5756,9 +5756,7 @@ def page(args: list[str], **kwargs: Any) -> Any: mock.patch.object(ve, "NEAR_EXPIRY_PAGE_CAP", 1000), mock.patch.object(ve, "NEAR_EXPIRY_ID_READ_CAP", 1000), mock.patch.object(ve, "_elapsed", return_value=0.0), - mock.patch.object( - ve, "_listing_clock", side_effect=lambda: now[0] - ), + mock.patch.object(ve, "_listing_clock", side_effect=lambda: now[0]), mock.patch.object(postmerge, "gh_capture", side_effect=listing), mock.patch.object(ve, "gh_api_with_headers", side_effect=page), contextlib.redirect_stderr(stderr), @@ -7287,18 +7285,13 @@ def setUp(self) -> None: self.fake = fake # The last tick's listing, read 15 minutes before this one. carried_at = self.START - timedelta(minutes=15) - self.carry = self.dir / "carry.json" - self.carry.write_text( - json.dumps( - { - "fetchedAt": carried_at.isoformat(), - "artifacts": [ - a - for a in fake.artifacts - if ve._when(a["created_at"]) <= carried_at - ], - } - ) + self.seed = json.dumps( + { + "fetchedAt": carried_at.isoformat(), + "artifacts": [ + a for a in fake.artifacts if ve._when(a["created_at"]) <= carried_at + ], + } ) for patcher in ( mock.patch.object(ve, "_wall_clock", side_effect=lambda: self.clock[0]), @@ -7375,12 +7368,13 @@ def answer(self, cmd: list[str], **kwargs: Any) -> Any: ) def environ(self, job: str) -> Any: + if job == "merge": + (self.dir / f"{job}-listing.json").write_text(self.seed) return mock.patch.dict( "os.environ", { "BABYSIT_RUN_CACHE": str(self.dir / f"{job}-run.json"), ve.LISTING_ENV: str(self.dir / f"{job}-listing.json"), - ve.LISTING_CARRY_ENV: str(self.carry), ve.TOKEN_ENV: "job-token", }, ) @@ -7499,7 +7493,8 @@ def test_enable_auto_merge(self) -> None: counts, refusals = self.arm_job() self.report("enable-auto-merge", counts) self.assertEqual(refusals, [None, *["verify:fail"] * 4]) - self.assertEqual(self.listings, 1) + # One whole listing for the five Readers, and its top-up's first page. + self.assertEqual(self.listings, 2) class EvidenceCommandTest(unittest.TestCase): From 6154cf167f43d553bfe658a898f4bf85957332ad Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:48:11 -0500 Subject: [PATCH 2/2] test(source-control): start the merge-job listing fixtures with no saved listing The merge job's listing file starts empty each run, so the fixtures no longer seed a listing from an earlier tick. The skip filter now reads a whole listing, and the listing counts include its top-up's first page. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_babysit_verify_evidence.py | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_verify_evidence.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_verify_evidence.py index d05cff608f5..49600424e4c 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_verify_evidence.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_verify_evidence.py @@ -7245,9 +7245,8 @@ def test_the_next_sweep_downloads_only_the_new_fails(self) -> None: class APrMergeTickListsOnlyBeforeAnAllowOrAKeep(unittest.TestCase): """Artifact listings and requests per pr-merge tick, each job's printed as a `babysit-gh-calls` line, in a repository shaped like 2026-10-27's: 6,240 - artifacts uploaded four a minute, and the listing the last tick saved 15 - minutes before as the carried base. The merge job's skip filter lists - first; then 17 gate candidates 20 seconds apart while uploads go on: + artifacts uploaded four a minute, and each job starting with no saved + listing. The merge job's skip filter lists first; then 17 gate candidates 20 seconds apart while uploads go on: eight refused by a FAIL at their head, seven whose verdict allows and whose risk score then refuses, two allowed; with one queued entry that each gate judges again once its keep is over `run_cache.MAX_AGE` old, or @@ -7283,16 +7282,6 @@ def setUp(self) -> None: fake.records[fail] = record(n, sha, verdict="fail", head_ref=f"b{n}") self.upload(fake, fail, run_id, self.START - timedelta(minutes=40)) self.fake = fake - # The last tick's listing, read 15 minutes before this one. - carried_at = self.START - timedelta(minutes=15) - self.seed = json.dumps( - { - "fetchedAt": carried_at.isoformat(), - "artifacts": [ - a for a in fake.artifacts if ve._when(a["created_at"]) <= carried_at - ], - } - ) for patcher in ( mock.patch.object(ve, "_wall_clock", side_effect=lambda: self.clock[0]), mock.patch.object(run_cache, "now", side_effect=lambda: self.clock[0]), @@ -7368,8 +7357,6 @@ def answer(self, cmd: list[str], **kwargs: Any) -> Any: ) def environ(self, job: str) -> Any: - if job == "merge": - (self.dir / f"{job}-listing.json").write_text(self.seed) return mock.patch.dict( "os.environ", { @@ -7395,10 +7382,10 @@ def merge_job( mock.patch.object(gh, "run_command", side_effect=self.answer), ): # The skip filter (babysit_merge_skip.py filter) lists first, on - # the carried listing, and saves it for the gate. + # an empty job listing, and saves it for the gate. skip = ve.Reader(REPO, "main") skip.listing() - self.assertEqual(skip.listing_mode, "incremental") + self.assertEqual(skip.listing_mode, "full") self.skip_calls = gh.gh_call_counts() refused = iter(self.refused()) for i, kind in enumerate(kinds): @@ -7461,9 +7448,10 @@ def test_the_merge_job_with_an_empty_queue(self) -> None: self.assertEqual( refusals.count(None), len(self.KINDS) - self.KINDS.count("fail") ) - # The skip filter's listing and each allowed candidate's own: no - # refused candidate lists, whatever the saved listing's age. - self.assertEqual(self.listings, 1 + self.KINDS.count("allow")) + # The skip filter's whole listing and its top-up's first page, and + # each allowed candidate's own: no refused candidate lists, whatever + # the saved listing's age. + self.assertEqual(self.listings, 2 + self.KINDS.count("allow")) def test_the_merge_job_with_a_queued_entry(self) -> None: counts, refusals = self.merge_job(queued=True) @@ -7481,13 +7469,14 @@ def slow_merge_job(self, queued: bool) -> int: return self.listings def test_a_slow_merge_job_with_an_empty_queue(self) -> None: - # The skip filter's listing, and the allowed candidate's own. - self.assertEqual(self.slow_merge_job(False), 2) + # The skip filter's whole listing and its top-up, and the allowed + # candidate's own. + self.assertEqual(self.slow_merge_job(False), 3) def test_a_slow_merge_job_with_a_queued_entry(self) -> None: # The queued keeps at 6 and 10 minutes find the listing past the # bound and list again; the allow at 12 lists on its own. - self.assertEqual(self.slow_merge_job(True), 4) + self.assertEqual(self.slow_merge_job(True), 5) def test_enable_auto_merge(self) -> None: counts, refusals = self.arm_job()