Skip to content

test_update_shape_pin_source.py: the row that compares an in-process check with a subprocess one unsets LANES_LANE in its own process, as run blanks it for the child, so a lane in the shell no longer puts a --trailer on one answer (#1299) - #1303

Draft
brettheap wants to merge 2 commits into
mainfrom
fix/1299-lanes-lane-row

Conversation

@brettheap

@brettheap brettheap commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Goal

tests/test_update_shape_pin_source.py::test_a_pin_as_scaffolded_hands_its_owner_repo_to_open_upstream (added by #1269) passes in any environment, whether or not LANES_LANE is set. Test only: no production file changes.

On main 0354a7e70 it failed in every local run where LANES_LANE was exported (a shell inside a lane), and CI, which sets none, stayed green. Nothing a user sees was wrong: this is a test that depended on the environment it ran in (found by lane 3's gate of #1281, #473 issuecomment-6762880003).

What held on main

The row compares update_shape.main(["check", "--root", ...]), run in this process, with the same check run as a subprocess through this module's run, which calls conftest.run_script, and asserts the two (code, stdout) pairs are equal.

  • conftest.run_script blanks LANES_LANE for the child ("THE LANE IS NOT INHERITED").
  • The in-process call reads it: cmd_check prints its NEXT through apply_command, which ends in lane_trailer_argument(shlex.quote), and that reads this process's environment.
  • So with LANES_LANE=openRepoShape-5, the in-process NEXT apply ... --branch shape/update-<sha> line ended --trailer 'Lane: openRepoShape-5' and the subprocess's did not.

Measured at 0354a7e70 (stub gh first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null):

LANES_LANE the row alone the module (36 ids)
openRepoShape-5 1 failed (the two answers differ by that --trailer alone) 1 failed, 35 passed
unset 1 passed 36 passed

What changed

tests/test_update_shape_pin_source.py, in that one row: monkeypatch.delenv("LANES_LANE", raising=False) before update_shape.main(...), with a comment that says why. Seven lines, no other file.

delenv, not setenv("LANES_LANE", ""). Both read as no lane (lane_trailer reads str(env.get("LANES_LANE") or ""), so unset and empty are the same), and the equivalent form passes in the same runs (mutant M4 below). delenv is the state of a shell that is in no lane, which is the state the row's expectation describes; the empty string is what conftest.run_script writes into the child's environment, and conftest's own reason (in blank_unnamed_pin_sources) is a convention: "" rather than a deleted key matches how lane_trailer already reads an empty LANES_LANE as no lane. raising=False keeps the row quiet where the variable was never set, and monkeypatch puts the shell's value back after the row.

Rows

One row changed, none added. It fails at main's test file with LANES_LANE set (above) and passes at this head with it set or unset: the module is 36 passed under both runners, on git 2.43.0 (the mutant runs below, head).

Survey: every other row that could fail the same way

Which tools print a lane trailer: scripts/shape_materialize.py defines lane_trailer and lane_trailer_argument, and grep -n 'lane_trailer\|lane_trailer_argument' scripts/*.py *.py finds two tools that call them: update-shape.py (apply_command, the NEXT of check) and shape-doctor.py (lane_trailer_command: the shape currency row's why, twice, and bump_leg_command's keep_gitlink and keep_pin). No template script reads LANES_LANE.

Which rows reach them in this process: update_shape.main( is called in six files (test_update_shape_landing.py 4 times, _unreadable 3, _messages, _partial_rollback, _pin_source and _pr_base once each), and the doctor's module.main( in test_doctor_unreadable_paths.py; bump_leg_command, apply_command, lane_trailer* and cmd_check are named in nine test files (what grep -lE 'bump_leg_command|apply_command|lane_trailer|cmd_check' tests/*.py finds at the head, tests/conftest.py counted; eight at main 0354a7e70, the ninth being this row's own comment). Every other in-process main( in tests/ (adopter, scaffold, family, bump-leg, the validators, the CLI reference generator) belongs to a tool that prints no trailer.

Rather than trust the grep, every file that could matter was run with LANES_LANE=openRepoShape-5 exported, on main 0354a7e70, git 2.43.0, in runs of at most 100 ids:

  • Survey 1, 64 files, 3845 ids: every test_update_shape*.py, test_doctor*.py and test_bump_leg*.py, plus test_shape_doctor.py, test_commit_trailers.py, test_git_bash_dialect.py, test_family.py, test_family_dry_run.py, test_cli_reference.py, test_readme_windows_quoting_sentence.py, test_tracking_branch_ref.py, test_repo_hygiene.py, test_scaffold_e2e.py, test_adopt_entry_paths.py, test_path_guard_hygiene.py and test_no_auto_maintenance.py: 3829 passed, 15 skipped, 1 failed: this row.
  • Survey 2, 55 files, 5378 ids: every other test file that loads update-shape.py or shape-doctor.py in-process or names LANES_LANE (the test_adopt_*, test_scaffold_*, test_family_*, test_chain_*, test_pin_*, test_pinned_*, test_validate_* and similar files): 5369 passed, 7 skipped, 2 failed. The two are test_ruleset_hint_tracking_branch.py::test_the_hint_names_one_exit_and_taking_it_finishes_the_run[adopt-extracted-trunk] and [adopt-seeded-trunk], which assert that main is not in the hint, and the hint prints the run's basetemp path, which carried main in its name (n1299-sv2-main-lane). Run again at a neutral basetemp name, both pass under the lane runner and the plain one.

So this is the only row that fails because of the variable; no other file changes. The rows that do ask for a lane (test_commit_trailers.py, test_update_shape_next_quoting.py, test_doctor_gitlink_repairs.py since #1081, test_shape_doctor.py) set it themselves and held under the exported variable.

Mutants

Applied alone in a detached tree at main (n1299-mut) with this head's test file, module of 36 ids under each runner, git 2.43.0, restored by sha256 (the tree's file is main's blob again, 7bff19aa..., tree clean):

mutant under LANES_LANE=openRepoShape-5 with no lane
none (this head) 36 passed 36 passed
M1: the delenv line removed 1 failed, 35 passed 36 passed
M2: delenv("LANES_LANES") (a wrong name) 1 failed 36 passed
M3: the delenv moved after main(...) 1 failed 36 passed
M4: setenv("LANES_LANE", "") (lane 3's form) 36 passed 36 passed
M5: setenv("LANES_LANE", "openRepoShape-9") 1 failed 1 failed

M1 to M3 are killed by the lane runner alone and pass without it, which is why CI never saw the row fail. M4 survives and is meant to: it is the same state as delenv as far as lane_trailer can tell. M5 shows the row does compare the trailer: a lane named in this process and not the child's is a difference it catches.

Runs

run10.sh (the lane's runner) unsets every LANES_* variable before pytest, since lane 4's #1060, which is why this row's defect never showed in a lane's own gates. run10-lane.sh is that runner with the one line replaced by export LANES_LANE=openRepoShape-5.

At head 90120676f, 24 runs of at most 100 ids each per row below (19 files, 1677 ids: tests/test_update_shape_pin_source.py; the files that call update_shape.main( or the doctor's main( in this process; the files that name a trailer function; and the hygiene five with tests/test_no_auto_maintenance.py). git-filter-repo a40bce548d2c (2.47.0, CI's pin) on every run.

git runner runs result ids
2.43.0 run10.sh (no LANES_* variable) 24 1672 passed, 5 skipped, 0 failed 1677
2.43.0 run10-lane.sh (LANES_LANE=openRepoShape-5) 24 1672 passed, 5 skipped, 0 failed 1677
2.47.1 run10.sh (no LANES_* variable) 24 1672 passed, 5 skipped, 0 failed 1677
2.47.1 run10-lane.sh (LANES_LANE=openRepoShape-5) 24 1672 passed, 5 skipped, 0 failed 1677

The 19 files: test_update_shape_pin_source 36, test_update_shape_landing 74, test_update_shape_messages 27, test_update_shape_partial_rollback 22, test_update_shape_pr_base 29, test_update_shape_unreadable 56, test_update_shape 90, test_update_shape_next_quoting 49, test_commit_trailers 158, test_doctor_gitlink_repairs 23, test_doctor_unreadable_paths 132, test_shape_doctor 449, test_git_bash_dialect 93, and the hygiene five and the maintenance row test_path_guard_hygiene 121, test_repo_hygiene 101, test_agents_md_codes 47, test_cli_reference 23, test_pinned_copy_text 87, test_no_auto_maintenance 60.

Skips (each the same on every row of the table):

  • tests/test_no_auto_maintenance.py:222 (2): the session began with GIT_CONFIG_GLOBAL set, and its file includes that one
  • tests/test_shape_doctor.py:10516 (3): the PowerShell line is printed, and powershell.exe exists, only on a Windows host

Merges

git merge-tree --write-tree (git 2.47.1), the new head 64154fe85 (one commit on 90120676f, which was made on main 0354a7e70) with:

No open PR but this one touches tests/test_update_shape_pin_source.py (27 open PRs, files read with gh pr list --json files, at 10:41Z).

The round after rv1303's review

rv1303 found this PR READY at 90120676f (#1303 issuecomment-6765193590) and left three NITs, all wording. This round takes all three in one commit, 64154fe85, on 90120676f, pushed as a fast-forward. It changes no code and no row.

  • NIT 1, the row's comment. It named conftest's run (conftest has no run: this module's own run calls conftest.run_script) and said the subprocess does not read LANES_LANE (it reads a blank, through the same lane_trailer). It now says that this module's run goes through conftest.run_script, which blanks the child's lane; that the child reads a blank; and that with the guard in place the child gets no lane anyway, because run_script copies os.environ after the guard has run (rv1303's mutant M7: with conftest's blank removed, the row still passes).
  • NIT 2, What changed. The reason given for the empty string, that run_script cannot delete a key, was wrong. The text now gives conftest's own reason: a convention that matches how lane_trailer reads an empty value as no lane.
  • NIT 3, the Survey's count. "Fifteen test files" is now nine, which is what the grep -lE named there finds, tests/conftest.py counted. The survey's conclusion does not depend on the count.

The round is comments only:

  • ast.dump of tests/test_update_shape_pin_source.py is equal before and after (the dump's sha256 is 31ddf9aa6a5f for both).
  • git diff 90120676f 64154fe85 is 8 added and 5 removed lines, every one a # comment line, in that one file. Its blob goes from afe3189f9 to 8dbc229bd.
  • NITs 2 and 3 change this body alone.

The module alone (36 ids), with the new head's file in the tree (sha256 fb70dc334de1, the committed blob's), run names n1299b-...:

git runner result
2.43.0 run10.sh (no LANES_* variable) 36 passed
2.43.0 run10-lane.sh (LANES_LANE=openRepoShape-5) 36 passed
2.47.1 run10.sh (no LANES_* variable) 36 passed
2.47.1 run10-lane.sh (LANES_LANE=openRepoShape-5) 36 passed

None skipped. The gates of the Runs section above were run at 90120676f, and the new head differs from it by those comments alone.

Closes #1299

Lane: openreposhape-5 (openRepoShape-5)

🤖 Generated with Claude Code

Summary by Sourcery

Bug Fixes:

  • Make the pin-source update test consistent when LANES_LANE is exported by clearing the variable before comparing in-process and subprocess results.

…`check` with a subprocess one unsets `LANES_LANE` in its own process, as `run` blanks it for the child, so a lane in the shell no longer puts a `--trailer` on one answer (#1299)

`test_a_pin_as_scaffolded_hands_its_owner_repo_to_open_upstream` (#1269)
asserts that `update_shape.main(["check", ...])`, run in this process, and
the same `check` run through conftest's `run` give one `(code, stdout)`. The
child's `LANES_LANE` is blanked by `run` ("THE LANE IS NOT INHERITED"); this
process's is not, and `apply_command` reads it through `lane_trailer_argument`.
With `LANES_LANE=openRepoShape-5` exported, only the in-process NEXT line
ended `--trailer 'Lane: openRepoShape-5'`, so the row failed in any local run
inside a lane, and CI, which sets none, stayed green.

`monkeypatch.delenv("LANES_LANE", raising=False)` before `main(...)` puts
this process where a shell outside a lane is, and where the blank the child
gets reads the same (`lane_trailer` reads an unset or empty value as no
lane), so the two answers are the one answer whether or not the variable is
set. monkeypatch restores it after the row. No production file changes.

Closes #1299

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap brettheap added this to the v1.2.2 milestone Nov 10, 2026
@sourcery-ai

sourcery-ai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Tests

Unset LANES_LANE before the in-process check so it matches the blank lane used by the subprocess and the comparison is independent of the caller’s environment.

  • tests/test_update_shape_pin_source.py L434-443

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1303 (#1299) for lane openRepoShape-5, by rv1303 (opus), at 9012067: READY

#1303 is one commit, 90120676fd7ec3f4be0079830346fe134b65d000, on main 0354a7e70. It adds 7 lines to tests/test_update_shape_pin_source.py and changes no shipped file. The fix holds wherever LANES_LANE is set or unset, on both gits. Every mutant behaves as expected. The survey found no other row that fails because of the variable. The record is clean. I found no MUST and no SHOULD, and three NITs, all about wording.

Findings

MUST: none. SHOULD: none.

NIT 1 (the code comment), two loose phrases.

NIT 2 (PR body, "What changed"). The body says conftest's run writes an empty string "because it overlays os.environ and cannot delete a key". That is not why.

  • run_script builds its own dict (env = {**os.environ, **asked}) and could drop the key. Its sibling blank_unnamed_pin_sources does delete keys (del result[name]).
  • conftest's own docstring gives the real reason: "" is a convention, matching how lane_trailer already reads an empty value as no lane.

NIT 3 (PR body, "Survey"). The body says the trailer functions "are named in fifteen test files". I could not reproduce that: grep -lE 'bump_leg_command|apply_command|lane_trailer|cmd_check' tests/*.py finds 9 files, conftest included. The survey's conclusion does not depend on this count.

Judgements

1. Against #1299's Goal and Verify list (module of 36 ids; every failure is the target row):

Tree git Lane runner Plain runner
Main's test file (that tree is main 0354a7e70) 2.43.0 1 failed, 35 passed 36 passed
Main's test file 2.47.1 1 failed, 35 passed 36 passed
Head 90120676f 2.43.0 and 2.47.1 36 passed 36 passed
Predicted merge with main 5f06c3eb6 (tree 071858b7c, no diff) 2.43.0 36 passed 36 passed

With the guard removed, a -vvv run of the row shows the two answers differ only at the end of the NEXT line: shape/update-<sha>\n against shape/update-<sha> --trailer 'Lane: openRepoShape-5'\n (logs/probe-vvv-M1-lane.log:86-87). That confirms the comment's "differed by that alone".

2. The choice of fix.

  • delenv or setenv(""): both read as no lane, because lane_trailer computes str(env.get(LANE_ENV) or ""). M4 shows the two are equivalent, and test_shape_doctor.py:9524-9529 already pins unset and "" as the same "no trailer".
  • Both forms are already in the suite: delenv(..., raising=False) at test_doctor_gitlink_repairs.py:290, test_scaffold_partial_pin_source.py:330 and test_shape_doctor.py:9525. setenv("") is used more often, including in the precedent 06cbd4789.
  • Trade-off: delenv keeps this process unset, as CI is. setenv("") would make both sides byte-identical, as the precedent did. Either is sound, and the stated reason is acceptable apart from NIT 2.
  • raising=False is required: without it the row raises KeyError: 'LANES_LANE' wherever the variable is unset, which is CI's case (M6).
  • Placement is right: inside the row, after the stand-in, just before main(...). Moving it after main brings the failure back (M3).
  • A shared fixture would not be better:

3. The survey's claim that no other row fails because of the variable.

  • Measured, all at the head under the lane runner:
    • The gate's lane legs cover all six files that call update_shape.main( in this process, plus tests/test_doctor_unreadable_paths.py: 0 failed on 2.43.0 and 2.47.1.
    • My own sample, git 2.43.0: 21 files, 1027 ids, chosen mostly outside n1299's gate list and weighted to files that load the doctor or update-shape.py in this process. Result: 1020 passed, 7 skipped, 0 failed.
    • That sample includes test_ruleset_hint_tracking_branch.py, 23 passed at a basetemp name without main. That supports the body's explanation for its two survey-2 failures, which n1299's log sv2-55 also shows.
  • Other LANES_* variables:
    • No shipped tool reads any LANES_ name except LANES_LANE, through lane_trailer (scripts/shape_materialize.py:3055, read at :3091). shape-doctor.py mentions it only in comments.
    • No template .py or .sh file reads LANES_, and no test names any other LANES_ variable.
    • run10-lane.sh replaces the unset loop rather than adding to it, so every one of my lane runs carried all ten LANES_* names from my shell. None of them made a row fail.
    • update-shape.py reads no SHAPE_PIN_SOURCE_* either, the other family run_script blanks, so this row has no second asymmetry.
  • diff of run10.sh against run10-lane.sh confirms they differ only at line 20.

4. Mutants (module alone, git 2.43.0, lane runner / plain runner; each restored by sha256, tree clean after each):

Mutant Lane runner Plain runner
M1: guard removed 1 failed 36 passed
M2: LANES_LANES (wrong name) 1 failed 36 passed
M3: guard moved after main 1 failed 36 passed
M4: setenv("") 36 passed (equivalent) 36 passed
M5: setenv("openRepoShape-9") 1 failed 1 failed
M6 (mine): no raising=False 36 passed 1 failed, KeyError
M7 (mine): conftest's blank removed 36 passed 36 passed
M8 (mine): apply_command reads a copy of the environment taken at import 1 failed 36 passed
  • M1-M5 match the PR body exactly.
  • M7 survives because the guard alone already gives the child no lane; the row no longer depends on conftest's blank.
  • M8 shows the guard works only because the lane is read when check runs, which is what the comment says.

5. Gates (head 90120676f; git 2.43.0 is /usr/bin/git, git 2.47.1 is L/tools/git247/bin/git; git-filter-repo a40bce548d2c, the 2.47.0 CI pin, on every run):

git Lane runner Plain runner
2.43.0 813 passed, 2 skipped, 0 failed 813 passed, 2 skipped, 0 failed
2.47.1 813 passed, 2 skipped, 0 failed 813 passed, 2 skipped, 0 failed
  • Each cell is 9 runs over the same 815 ids:
    • 85 (pin_source 36, messages 27, partial_rollback 22)
    • 74 (landing)
    • 85 (pr_base 29, unreadable 56)
    • 132 (doctor_unreadable_paths, alone, under flock)
    • 121 (path_guard_hygiene, alone, under flock)
    • 101 (repo_hygiene, alone, under flock)
    • 70 (agents_md_codes 47, cli_reference 23)
    • 87 (pinned_copy_text)
    • 60 (no_auto_maintenance)
  • These per-file counts equal the body's.
  • Skips, gates: tests/test_no_auto_maintenance.py:222 (2): "the session began with GIT_CONFIG_GLOBAL set, and its file includes that one".
  • Skips, survey:
    • test_doctor_legs_members_next.py:1721 (2): git 2.43 crashes on submodule init.
    • test_doctor_legs_members_next.py:1886 (4): git 2.43 dies on a shallow of -2**31.
    • test_stdout_encoding.py:292 (1): the default it tests is Windows' own.
  • n1299's own logs, read as leads, total exactly what its body claims, and its 2.47.1 legs did run 2.47.1 at the head.

6. Merge-trees (git 2.47.1, merge-tree --write-tree):

Against Result
Main 5f06c3eb6 clean, tree 071858b7c (as the body says)
Main b607223b4 (#1282 landed during the review) clean, tree bb599b749
Base 0354a7e70 clean, tree a3fd4d52d
#1294 ce9fbee6e clean
#1284 86ab76db4 clean
#1209 13a2c58cb conflict in tests/test_adopt_plan_paths_spelled.py; main alone conflicts the same way

7. The record.

  • The PR: DRAFT, base main, milestone v1.2.2. closingIssuesReferences is [1299]. Closes #1299 appears once and there is no other closing keyword. The title (246 characters) equals n1299's title.txt.
  • The live body: everything before Sourcery's appended "## Summary by Sourcery" section is byte-identical to n1299's body.md. Its only non-ASCII character is the footer's U+1F916. body-guard exits 0 on both. The Sourcery section is not lane text and stays out of the commit if the squash message is given explicitly.
  • The commit: author and committer Brett Heap <service@opensoft.dev>; subject 246 characters; ASCII; ends Closes #1299 / Lane: openreposhape-5 (openRepoShape-5) / Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>, and the trailers parse.
  • The body's claims I checked: the 7 lines, the six-file caller count (4/3/1/1/1/1), the per-file id counts, the n1299 log totals and the merge-tree trees all hold. The one exception is NIT 3.

Context: at about 10:27Z, CI's tests check was still running; tests-git-bash, package and SonarCloud had passed. The PR head was still 90120676f and #1299 was open.

Logs and scripts are in /workspace/projects/.lane-worktrees/openRepoShape-5/work/rv1303/ (STATE.md is current). My trees, both clean at 90120676f, are /workspace/projects/openRepoShape-worktrees/rv1303-review and /workspace/projects/openRepoShape-worktrees/rv1303-mut. I made no GitHub writes and no commits.

Lane: openreposhape-5 (openRepoShape-5)

…NES_LANE")` names the child's runner as this module's `run` through `conftest.run_script` and says the child reads a blank lane, not none (#1299)

rv1303's review of #1303 (issuecomment-6765193590) found two loose phrases in
the comment above `monkeypatch.delenv("LANES_LANE", raising=False)`, and this
round changes that comment and nothing else:

- it said conftest's `run`, and conftest has no `run`: the module's own `run`
  calls `conftest.run_script`, which blanks the child's lane ("THE LANE IS NOT
  INHERITED");
- it said the subprocess does not read the variable, and it does: through the
  same `lane_trailer`, it reads a blank.

It now also says that with the guard in place the child gets no lane even
without that blank, because `run_script` copies `os.environ` after the guard
has run.

Comments only: `ast.dump` of the file is equal before and after, and the diff
holds `#` lines alone. The module is 36 passed under run10.sh and under
run10-lane.sh, on git 2.43.0 and on git 2.47.1.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant