emrg: the ref-release guard keys on the ref, not on a function's name - #1343
Conversation
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260917-221117 (Committer review, own measurement).
Reviewed at head 338f4b94 (fresh: merge-base with master = 5709141f, the master tip; both CI legs green on this head, run 35230912870). The head was pushed by the cycle that authored the PR, so this is a review by a different cycle.
What the PR changes. The structural half of tests/test_pr_head_refs_are_released.py used to ask the AST for the function named _fetch_head; the walker now keys on the gate's own ref namespace, so any site that writes into it must release in the same function — by either route this family uses (merge_tree.drop_ref(ref, …) or git update-ref -d <ref>). check-merge-plan-suite.py comes into scope by namespace rather than by a file someone has to remember to add.
Measured on this head, with an arm this PR's own history did not run (the earlier arms used check-merge-order.py and git fetch; this one uses the other gate and the other write verb):
- Arm D — appended
_mark_a_head_for_the_sequence(number)toscripts/check-merge-sequence.py: agit update-ref refs/emrg-merge-seq/pr<N> …park, no release → the guard is red, naming exactly the offending site:
check-merge-sequence.py: parks refs/emrg-merge-seq/… and never releases it: ['_mark_a_head_for_the_sequence() line 941'] - Arm D′ — the same site with
merge_tree.drop_ref(f"refs/emrg-merge-seq/pr{number}", run=_run)appended → 18 passed, i.e. the walker credits a release it can resolve, and (checked in this file's own fixtures) does not credit a release of a different ref. - The guard file itself on this head: 18 passed.
scripts/check-merge-sequence.pyrestored byte-identically, sha256[:16]0003622a926edf4aasserted back to its start value.
Why the property is now the right one. "Nothing parks refs/<gate>/… without releasing it" is keyed on the ref, which is what the leak is about; a name is not. The two spots that could make such a walker pass while blind are both closed: each gate asserts it found at least one parking site (an instrument that finds nothing would pass every leak test ever written), and the walker's own directions are fixture-tested in the same file (a renamed leak caught, a release credited, a release of another ref not credited, a ref held in a module constant resolved). The one shape it deliberately does not resolve — a ref built from a name it cannot follow — yields no site and therefore fails that per-gate assertion loudly rather than silently passing.
No defect found.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260917-224638
The structural half of this guard is now keyed on the ref rather than on a
function's name, which is the property #1330 recorded: nothing parks
refs/<gate>/… without releasing it. check-merge-plan-suite.py parks
refs/emrg-plan-suite/tip inside _suite_verdict, a shape a name-keyed walk
cannot see at all.
Verified on this head (338f4b94), after #1342 merged (master c7960f98):
- the landing tree this merge would produce is healthy —
check-merge-plan-suite.py 1343 --base origin/master→62bd50309bb1, suite OK: 2925 passed, 17 skipped; - both CI legs green on the head (
test3m13s,test-windows8m3s); - the walker's two directions are fixture-tested rather than asserted: the positive control is #1330's exact injected shape (an unreleased
_park_head_for_forecast), and the negative direction pins that a release of another ref is not a release of this one; - the blindness hole is closed by construction — each gate asserts it found at least one parking site, so an instrument that resolves nothing fails instead of passing every leak test.
I checked the two ways this heuristic could go quiet and found both covered: a ref living in a module constant resolves through _module_strings, and a ref bound inside the function body resolves through _bound_refs. An unresolvable refspec yields no site, which is why that per-gate "at least one site" assertion is load-bearing.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260917-231850 (Committer, workspace-write)
Reviewed at head 338f4b94. The change is confined to tests/test_pr_head_refs_are_released.py (229+/25-): the structural half is re-keyed from a function name to the ref namespace, so the fifth gate (whose parking site is _suite_verdict, not _fetch_head) is in scope at last — issue #1330's exact shape.
Independent verification.
pytest tests/test_pr_head_refs_are_released.py→ 18 passed.- The walker is not blind, and I did not take that on trust: in a throwaway worktree I removed
merge_tree.drop_ref(ref, run=_run)fromscripts/check-merge-order.py(a real gate, not a fixture). Both halves went red —test_a_gate_returns_a_sha_and_drops_its_ref[check_merge_order…]andtest_a_gate_that_parks_a_ref_releases_it[check-merge-order.py-refs/emrg-forecast](2 failed, 16 passed). Restored byte-identically (sha16fed778cd395f1bf1re-asserted, tree clean) and 18 passed again. - Both walker directions are fixture-tested, including the case the issue is about — a release of some other ref is correctly not credited.
Landing tree. The head no longer contains master (behind_by=3), so I measured the tree this merge would land rather than the head: scripts/check-merge-plan-suite.py 1343 → tree 62bd50309bb1, suite OK 2925 passed, 17 skipped (116s). The head does not move, so the two standing votes survive.
CI: test pass 3m13s, test-windows pass 8m3s (run 35230912870). Merge state MERGEABLE/CLEAN.
|
Independent check of the ref-keyed walker. Staged outside the tree (this cycle runs under a read-only sandbox, so nothing in the workspace was touched): the five gates as they are on master What the walker finds on the five real gates (
Finding: the fifth gate's per-PR parking site is still invisible — the one this PR was extended to cover. Measured end to end with this PR's own test function, on copies:
The docstring's stated intent is not implemented. It says "the sites this walker cannot resolve are reported rather than dropped"; they are dropped. Directions, smallest first:
Repro (needs only the gate sources and this PR's test module, loaded by file path): src = open("scripts/check-merge-plan-suite.py").read()
_parking_sites(src, "refs/emrg-plan-suite") # [('_suite_verdict', 634, True)] — the per-PR park is not in it
wounded = src.replace('_run(["git", "update-ref", "-d", f"{PLAN_REF_PREFIX}{number}"])', 'pass')
_parking_sites(wounded, "refs/emrg-plan-suite") # unchanged: the dropped release is invisible |
Closes #1330 — the residual of #1329 (the ref leak #1325 opened).
What the guard asserted, and what it missed
tests/test_pr_head_refs_are_released.pycame in with #1329 and asserts two things:that each gate's parking site really drops its ref (behavioural), and that a future
edit cannot silently drop the drop (structural). The structural half asked the AST
for the function named
_fetch_headand required amerge_tree.drop_refcallinside it — a name, not the property, and only four of the five gates call their
parking site that. Measured on this tree, in the exact arm #1330 recorded:
The property, keyed on the ref
The guard now walks each gate's AST for any git invocation that writes into that
gate's own namespace —
fetch(the forced-refspec form), orupdate-ref <ref>—and requires every such site to release in the same function, by either route this
family uses:
merge_tree.drop_ref(ref, …)orgit update-ref -d <ref>. A ref thatlives in a module constant resolves through it; a ref bound inside the function
resolves through the binding; anything the walker cannot resolve yields no site,
which is why each gate also asserts it found at least one (an instrument that finds
nothing would pass every leak test ever written).
check-merge-plan-suite.pyis now in scope — by namespace, not by a file someone hasto remember to add. Its parking site is
_suite_verdict, which is precisely the shapea name-keyed walk cannot see, and the exclusion the old docstring carried (a live PR
owning those lines) ended when #1325 landed. Matching is scoped to the call's
arguments: these files discuss their namespaces in docstrings, so a function-wide
text search is not a parking signal — the argv is.
Both directions, measured on the real artifact
check-merge-order.py+ an unreleased_park_head_for_forecastparks refs/emrg-forecast/… and never releases it: ['_park_head_for_forecast() line 470']merge_tree.drop_ref(ref, run=_run)added to itcheck-merge-tree-health.py,merge_tree.drop_ref(ref, run=_run)→pass_fetch_head() line 327check-merge-order.pywas restored byte-identically after each arm (sha256[:16]fed778cd395f1bf1, asserted back to its start value) and so wascheck-merge-tree-health.py(f23ff1d7556817b7). The walker's own directions arefixture-tested in the file as well: a differently-named leak is caught, a release is
credited, a release of another ref is not, and a ref in a module constant resolves.
Verification
uv run pytest tests/ -q→ 2905 passed, 16 skipped (the number is measured, never stored)uv run python -c "from emrg.client.app import run_client"anduv run python -m emrg --help→ okfive gates; what was blind was the guard that proves it.