Repository navigation
tests: one skip helper for names and branches that are not UTF-8, Windows guards on make_file and the --branch a\udcffb test by emulation, and the UTF-8 promise and the leg finding's role pinned (#663) - #723
Conversation
… carry a name or branch that is not UTF-8, `os.fsencode` first and the disk's EILSEQ second, and two pins hold `bump-leg.py`'s UTF-8 decode and the leg finding's role, so a test no longer skips on the disk alone or passes under a decode as ASCII or a finding named by path (#663) #461's five test lines, lane 2's L2-52 and lane 4's R423 N1 and N2 and R455 F1 and F3. Test only: no tool and no template changes, and `validate-manifest.py`, a pinned copy, is read and not edited. - `tests/conftest.py` holds `skip_where_a_name_cannot_be_not_utf_8`, which `tests/test_adopt_pins.py` and `tests/test_pin_source_unreadable.py` each had a copy of. It asks `skip_where_fsencode_gives_a_surrogate_no_byte` first (Windows' `os.fsencode` makes three bytes of U+DCFF, so no name that stands for a byte is carried there, though NTFS would take the file), then the disk (macOS answers EILSEQ); any other error is the test's own. - `make_file` and the `--branch a\udcffb` test call it, so they skip on Windows too, where the disk takes the name and the child then refuses the branch. `test_a_branch_the_system_carries_has_no_fault` asks the same function instead of its own copy of the question. - `test_a_name_that_is_utf8_under_quote_path_false_is_still_refused` asserts that the line naming `caf<e9>.yml` holds no literal `\x`, in any locale, so `bump-leg.py` decoding git's bytes as ASCII fails it. - `_leg_branch_findings` is asked of a leg whose role is not its path, so naming the leg by `path` fails it. - `tests/test_non_utf8_name_helpers.py` holds the helpers to this, with Windows' `os.fsencode` and macOS's EILSEQ emulated: CI is Linux only, and no Windows or macOS run was made. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…held by an emulation, so dropping the shared helper from it fails a test on Linux too (#663) `test_a_branch_the_system_carries_has_no_fault` now asks `skip_where_fsencode_gives_a_surrogate_no_byte` where it had its own copy of the question. The new test calls it under Windows' `os.fsencode` (emulated): a branch holding a byte-carrying surrogate is skipped, plain text and text with an accent run. Removing the call fails the two skipped rows. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-16T14:22Z) @codex review |
Reviewer's GuideTest-only changes consolidate non-UTF-8 platform guards, emulate Windows and macOS behavior on Linux, and add regression coverage for skip ordering, UTF-8 diagnostic rendering, and role-based manifest-leg findings; no tools, templates, or pinned production copies are modified. File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. What shall we delve into next? Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
… fails where a skip escapes it, `windows_fsencode` passes bytes through as `os.fsencode` does, and the no-`\x` check skips where the file name encoding is not UTF-8, so a leaked skip, an emulation that breaks on bytes and a Latin-1 host no longer pass silently or fail on a tool that is right (#663) The review of #723 (0b1d1dd) found no MUST and three things to take. SHOULD-1: `skip_message` had no test of its own. A skip that escapes it ends the test SKIPPED, and no run fails on a skip, so the mutant where one escapes gave `10 passed, 3 skipped` on the helpers file and seven emulation tests silently skipped. `tests/test_non_utf8_name_helpers.py` now holds it with three tests: it returns the reason a call skips with, it returns None where the call does not skip, and it lets any other exception go on. The first catches the escape and fails it with `pytest.fail`, because a bare assert on an escaped skip would end that test SKIPPED too. NIT-2: `windows_fsencode` took `str` only. `os.fsencode` passes bytes through on every system, so the emulation now does, and two tests hold it (the surrogate and text, and the bytes, compared with `os.fsencode` itself). NIT-1: on a Latin-1 file name encoding `make_file` writes `caf\xe9.yml` as the single byte 0xE9, the tool prints `caf\xe9.yml`, and the new `"\\x" not in shown` assertion would fail although the tool is right. It is now `assert_shown_without_hex_escapes`, which skips where `os.fsencode(UTF8) != UTF8.encode("utf-8")` and is called after the test's other assertions, so a skip loses none of them. Two tests hold it under an emulated Latin-1 `os.fsencode` (it skips) and Windows' (it asserts, and fails a line that holds a `\x`). Tests only; no tool and no template changes. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-bump-leg-branch-and-push-line #723 lands before #746, and the two conflicted in one assertion of tests/test_non_utf8_git_reads.py::test_a_name_that_is_utf8_under_quote_path_false_is_still_refused. This branch moved its prefix from four spaces and `M` to five (#672 keeps the leading space of git's ` M`). #723 rewrote the same `any(...)` into `[shown] = [...]`, `assert shown.endswith(".yml")` and a closing `assert_shown_without_hex_escapes(shown, output)`, so a decode of git's bytes as ASCII fails it. The resolution takes #723's form with this branch's five-space prefix. Each side alone fails here: #723's four-space line finds no line under #672's output (`ValueError: not enough values to unpack`), and this branch's `any(...)` drops the hex-escape check, so `bump-leg.py` decoding git's bytes as ASCII passes it. Every other hunk merged as git made it. Every test of each side stands: test_non_utf8_git_reads.py has #746's 14 and #723's 19 test functions as 19 (#723's file is a superset of names), test_bootstrap_branch_bytes.py 18, test_non_utf8_name_helpers.py 12, test_adopt_pins.py 44 and test_pin_source_unreadable.py 13. The three synthetic lines #723's helper tests feed `assert_shown_without_hex_escapes` keep their four spaces: they are the helper's input, not the tool's output. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s it in UTF-8 and the name holds no surrogate, says `did not try`, and on Windows git's bytes are read as UTF-8, so `feature/日本..x` or `"日\udcff"` under `LC_ALL=C PYTHONUTF8=0` is no longer called good, and the advisory test passes (#710) Fix round 1 for RV824's review of #824 (lane 3, issuecomment-6236261109), under the lane 4 coordinator's rulings. - M1: `_try_git`'s docstring named the advisory module, which `tests/test_shape_advisory.py` refuses in any template. It now says "in `shape-doctor.py` and in the advisory it reads". - S1: three changes, so the ruled line never calls a bad name good. - `_argument_fault` asks `UNWRITABLE` last, and names any surrogate in the name first, a byte-carrying one included, because `validate-manifest.py` names every surrogate (#280). - `_git_refuses_in_utf8` asks `git check-ref-format --branch` about the name's UTF-8 bytes before the line calls it good. `feature/日本..x`, `日本.lock` and `日本 x` get git's own words and #455's way out. - The new tests have rows for those three names and for `"日\udcff"`. - S2: `tests/test_bootstrap_branch_bytes.py`'s no-fault test skips `feature/日本`, `a<U+200D>b` and `a\xe9b` where `os.fsencode` cannot write them (`written`), and its docstring states the rule. The parameter list and docstring change; #723's lines do not. - S3: each `assert a and b` in the new test file is now two asserts. - N2: the first line for `UNWRITABLE` says `did not try`. - N3: on Windows `_try_git` decodes git's bytes as UTF-8, since `subprocess` hands git its arguments through `CreateProcessW`. `os.fsdecode` there is `mbcs` with `replace` under `PYTHONLEGACYWINDOWSFSENCODING=1`. #745's `_head_of` is not changed. - N4: a row sends `UNWRITABLE` through a leg's own `branch:`. - N5: the four new-file tests that hand git a name only UTF-8 can write, or read a child's UTF-8 line, skip where pytest itself is not under UTF-8. - N6: a comment at the decode records #710's ruling, Option 2: `git_out` keeps its strict UTF-8. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ewrite that `clone_just_pushed`, `read_through_git` and the adopt tests' `pasted` each built, and the empty-mount `submodule update --init` sites use it (#662) (#748) Stacked on #713 (`fix/645-fixture-copy-maintenance-race`, approved head `979bb394`). The branch head is `deb029c9`. Its history is my commit `4149cbb5`, a merge of `979bb394` (`5c06c115`, no conflicts), and a fix round after rv662's review (`deb029c9`). Against `979bb394` the diff is the two commits' files: `tests/conftest.py`, `tests/test_adopt_e2e.py`, `tests/test_clone_just_pushed.py`, `tests/test_doctor_legs_members_next.py` and `tests/test_family_bump_remove_rollback.py`. Retarget to main once #713 lands. The change is test only. - `tests/conftest.py` has one helper, `insteadof_prefixes(remotes, typed=False)`: the directory resolved, in both of a Windows path's spellings, each with a separator after it, and the directory as typed first when asked. `insteadof_entries` pairs each prefix with the one `url.<file URI>.insteadOf` key, and `insteadof_args` gives the same as `-c` arguments. - `clone_just_pushed` passes the arguments. `read_through_git` appends the entries to the `GIT_CONFIG_*` the environment already holds. It used to overwrite the count, which dropped #713's two settings from a pasted line's command scope. Its result is only right laid over `os.environ` at the same moment, as `pasted` does, and the docstring says so. - `pasted()` in `tests/test_adopt_e2e.py` was a third copy of the rule and now uses the helper. - `submodule_update_through_git(root, remotes, *options)` sits beside `clone_just_pushed` and runs `git submodule update` with the file protocol allowed and the helper's `-c` arguments. I traced every `git submodule update` the tests make (GIT_TRACE), before and after. A local clone shows as a `git clone` of a plain path with no `pack-objects`; the transport shows `pack-objects` and `index-pack` once per clone. - `test_family_bump_remove_rollback.py` :337 and :908: both legs of a member are cloned. Now the wrapper. - `test_doctor_legs_members_next.py` :631 (the `code` leg, store removed by `unbootstrap`), :987 (the member and its two legs) and :1054 (the member's two legs, `with_its_own_legs`): all clone. Now the wrapper. - `test_doctor_gitlink_repairs.py:343` and `test_doctor_unfinished_merge.py:145,171`: no clone and no fetch (the store exists). Unchanged. - `test_doctor_legs_members_next.py:607`: the deliberate failure probe. Unchanged. No rule separates a clone of the session's remotes from a clone of a test's own `source`. - What makes a clone racy is how the repository got its objects (a push, a commit, a copy), not how the call reads. The 55 `clone` call sites in tests/ name their source `source`, `bare`, `remote`, `origin`, `seed`, `work`. - In the adopt tests `source` is cloned plain before `execute` (a legal clone) and is the target of `execute`'s push after it (cloned then with `clone_just_pushed`). Only the order tells them apart. - The one spelling a name rule would catch, `remotes / f"{SPARE}.git"`, clones a `shutil.copytree` copy the test made itself. - The three `--recurse-submodules` hits assert on printed text; no recursive plain clone of a session remote is left. The real sites were `submodule update --init`, which the wrapper covers. - After #713 auto maintenance is off in every repository the suite makes. A limit: no test fails if a site goes back to a plain `submodule update --init`. rv662 notes a narrower test is possible (refuse any `"submodule", "update"` call outside conftest and a four-entry allowlist: the sites above that are left alone, and the probe at :607). It would guard six adoptions and was left out as a v1.1.1 choice. New cases in `tests/test_clone_just_pushed.py` (19 tests in the file now, 18 before the fix round): the prefix list for a typed and a resolved directory, each given with no trailing separator, "/" and the system separator; a symlinked directory; the entries and the `-c` form; `git ls-remote --get-url` through both forms (a plain path goes to its file URI, a neighbour called `remotes-other` is left alone); the environment form keeps the entries the environment holds; the environment form reads a path typed through a symlink (added in the fix round, `a_link_to` makes the link); an `--init` through the wrapper arrives as one pack, and the same `--init` plain leaves the remote's loose objects loose. - On `deb029c9`, after the merge and the fix round: `tests/test_clone_just_pushed.py` and `tests/test_no_auto_maintenance.py`: 30 passed; `tests/test_repo_hygiene.py`: 77 passed. - On `4149cbb5`, before the merge (the merge changes none of my files except the hunk positions in `conftest.py`): - `tests/test_family_bump_remove_rollback.py`, `test_doctor_legs_members_next.py`, `test_doctor_gitlink_repairs.py`, `test_clone_just_pushed.py`, `test_no_auto_maintenance.py`, `test_bump_leg_paste_lines.py`, `test_adopt_e2e.py`: 259 passed; - `test_scaffold_e2e.py`, `test_setup_project_py.py`, `test_setup_sh.py` (the `read_through_git` users): 279 passed; - `test_shape_doctor.py`: 405 passed, 3 skipped (the PowerShell line, Windows only); - the other 48 files that use `clone_just_pushed` or `cloned_just_pushed`, in two runs: 1980 passed, 9 skipped, and 1194 passed. - Mutants of the helper, the wrapper and the two users, each run against `tests/test_clone_just_pushed.py`. Each fails at least one new test: - no trailing separator on the prefixes: 9 failed (the separator cases, the `ls-remote --get-url` case and the held-entries case); - typed adds nothing, typed after resolved, and the key built from the typed URI: 1 failed each (the symlink case); - `read_through_git` overwrites the count: 1 failed (held entries); - the wrapper without the rewrite: 1 failed (the wrapper's one-pack case, `8 loose objects`); - `clone_just_pushed` without the rewrite: 2 failed (existing cases); - `read_through_git` with `typed=False` (rv662's N3, which survived at `5c06c115`): 1 failed on `deb029c9`, 18 passed, the new symlink case; unmutated 19 passed. `git merge-tree` is clean for `deb029c9` against origin/main `3726ada2` (and earlier `55c6ba7d` and `20c8873b`) and against #713's head `979bb394`. Checked at `4149cbb5`, before the merge, which changed none of my files: `fix/663-non-utf8-name-tests` `0b1d1ddb` (#723) and every `fix/*` and `l3-*` head on origin (35 refs). - Five tests build the same append-to-`GIT_CONFIG_COUNT` arithmetic by hand beside conftest's `appended_to_command_scope`: `command_scope` in `test_adopt_git_reads.py:91`, `test_adopt_ignored_shape_files.py:579` and `test_adopt_nested_attributes.py:377`, and inline in `test_bootstrap_after_bump.py:151` and `test_clone_just_pushed.py:96`. A follow-up. - Six `git clone --no-local` calls in tests build `clone_just_pushed`'s options by hand (`test_bump_leg_dry_run_truth.py:98,110`, `test_doctor_legs_members_next.py:1824,1925`, `test_doctor_pin_restore_merges.py:714`, `test_family_bump_to_checks.py:235`). rv662 checked that the ones it read are non-recursive. A follow-up. - `PASTE_ENV` (rv662's candidate, v1.1.1): `tests/test_doctor_legs_members_next.py:64` is a module-level snapshot of `os.environ` taken at import time with `GIT_CONFIG_COUNT` set to `"1"`, which overwrites. The 34 `paste(...)` calls run under neither #713's maintenance settings nor the `insteadOf` rewrite, and a pasted `git submodule update --init --recursive --checkout` still local-clones (`test_a_plain_clones_empty_mounts_are_cured_by_the_pasted_line` shows `git clone --no-checkout … remotes/Atlas-code.git` and no `pack-objects`). The same class as this issue, and not on its list. The fix is to build the paste environment with `read_through_git(remotes)` appended to `os.environ` at call time. **Review.** rv662 re-verified the fix round: APPROVE at `deb029c9`. The diff is the two asked-for changes only, mutant N3 now fails the new symlink test, and `merge-tree` is clean against `3726ada2` and `979bb394`. Review: rv662, lane 5's independent review, approved deb029c once its SHOULD-1, SHOULD-2 and NIT-1 were done. CI (Brett Heap's ruling 27): H0 deb029c passed tests, tests-git-bash and SonarCloud; the head 8368d1f merges #713's final head and then main after #713 landed as 741679e, taking the branch's own side for #713's files, with tests, tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree bc67aa8 over main 64ec47c: the adopt_e2e, adopt_plan_yaml_quoting, bootstrap_after_bump, bootstrap_leg_moves, bootstrap_without_git, clone_just_pushed, clone_url_bytes, doctor_legs_members_next, family, family_add_member_variant, family_add_spellings_remedies, family_bootstrap_gate_order, family_bump_remove_rollback, family_head_lost_gitmodules, family_members_case_variants, family_switch_branch_exists, local_remote_dir_urls, reader_refusals_named, shape_pin_row_path, siblings_inputs, validate_family_branch_step, validate_family_remedies_typed, yaml_indicator_openers, repo_hygiene, agents_md_codes, cli_reference sets, 1748 passed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts". Closes #662 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 6 hours and 50 minutes by commenting |
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…:refs/heads/<b>`, dirty names are spelled, `committed` lists what git holds, and the dry run says it runs no validator (#672) (#746) `bump-leg.py` read the root's branch with `--short`, printed a push line that `+` could turn into a forced push, printed control characters from file names raw, listed paths the commit did not hold, and said nothing about the validators a dry run skips. Each of #672's five lines was measured on main `35133f29` with a scaffolded project and local bare remotes, and each is gone on this head. One file of code changes: `scripts/bump-leg.py`. This branch now holds #723 (#663, `fix/663-non-utf8-name-tests`, head `6ba8c5d2`) and is stacked on it. #723 lands first. Until then this PR's diff against main also shows #723's six test files. Once #723 lands, the PR is retargeted and restacked, and its own diff is #672's again. One new commit, a merge commit: `4a014a79` merges `6ba8c5d2` into `a5d473ee`. It changes no tool code. `scripts/bump-leg.py` is byte for byte the same as at `a5d473ee`. ### The conflict The two PRs both changed one assertion in `tests/test_non_utf8_git_reads.py::test_a_name_that_is_utf8_under_quote_path_false_is_still_refused`: - **#746** changed its prefix from four spaces and `M` to five spaces and `M`, because #672 keeps the leading space of git's ` M`. - **#723** rewrote the same `any(...)` as `[shown] = [...]` and `assert shown.endswith(".yml")`, then added a closing `assert_shown_without_hex_escapes(shown, output)`. That check fails if `bump-leg.py` decodes git's bytes as ASCII. **The resolution** keeps #723's form and uses #746's five-space prefix. Neither side alone works here: | Taken as is | Result on the merge | |---|---| | the resolution | `2 passed` | | #723's line (four spaces) | `2 failed`: `ValueError: not enough values to unpack (expected 1, got 0)`; #672's output has no such line | | #746's `any(...)` | `2 passed`, but the hex-escape check is gone | | the resolution, with `bump-leg.py` decoding as ASCII | `2 failed`: `assert '\\x' not in ' M .git...xc3\\xa9.yml'` | | #746's `any(...)`, with `bump-leg.py` decoding as ASCII | `2 passed`: the mutant is missed | Every other hunk merged the way git merged it. ### Every test of each side stands Test functions per file, before and after the merge: | File | #746 | #723 | merge | |---|---|---|---| | `tests/test_non_utf8_git_reads.py` | 14 | 19 | 19 | | `tests/test_bootstrap_branch_bytes.py` | 15 | 18 | 18 | | `tests/test_non_utf8_name_helpers.py` | (none) | 12 | 12 | | `tests/test_adopt_pins.py` | 44 | 44 | 44 | | `tests/test_pin_source_unreadable.py` | 13 | 13 | 13 | #723's names include all of #746's, and every one is in the merge. The merge holds #723's other five files exactly as #723 has them. In `tests/test_non_utf8_git_reads.py` it differs from #723 only in #746's three hunks. #723's helper tests feed `assert_shown_without_hex_escapes` three made-up lines. Those lines still start with four spaces, because they are the helper's input, not the tool's output. **Each side's half is needed** (`tests/test_non_utf8_git_reads.py` has 52 tests): - The merged file run against #723's `bump-leg.py`, from before #672: `7 failed, 45 passed`. - #723's version of the file run against this branch's `bump-leg.py`: `7 failed, 45 passed`. In both runs the 7 failures are the tests that read the dirty list: - the two `refused_dirty` tests, in both modes; - the resolved test, in both modes; - `test_the_refusal_names_every_dirty_file_whatever_its_bytes`. ### Tests on the merge Run on the tree `4f4fed60`, which is the tree of `4a014a79`: - **#723's files, the helpers file and the trio:** `tests/test_non_utf8_git_reads.py`, `test_non_utf8_name_helpers.py`, `test_adopt_pins.py`, `test_bootstrap_branch_bytes.py`, `test_pin_source_unreadable.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`. Result: `504 passed`. - **The bump-leg files and the files that import `test_bump_leg`:** every `tests/test_bump_leg*.py` (eight files), plus `test_branch_left_bump_leg_setup.py`, `test_family_switch_branch_exists.py`, `test_update_shape_paste_lines.py`, `test_validate_pins_unreadable_workflows_dir.py` and `test_bootstrap_after_bump.py`. Result: `364 passed, 1 skipped`. The skip needs a disk that ignores case. ### Merge checks `git merge-tree --write-tree` of the merge: - **origin/main `24a977c0`:** clean. - **every remote `fix/*` and `l3-*` branch:** clean, except `fix/758-git-command-scope` (`b6e15591`), which conflicts in `tests/test_non_utf8_git_reads.py`. That conflict comes from #723: `6ba8c5d2` conflicts with `b6e15591` on its own, and `a5d473ee` does not. 1. **The branch is read in full.** `current_branch` ran `git symbolic-ref --short HEAD`. Beside a tag of the same name that prints `heads/<b>`. - Measured on main: with a tag `main` beside the tracking branch `main`, `bump-leg-on-tracking-branch` was not given. The bump committed on `main` and printed `push -u origin heads/main`. - A HEAD on `refs/tags/x` was read as the branch `x`, and the bump committed through the tag and moved it. - Now the full ref is read and `refs/heads/` is taken off. A ref outside `refs/heads/` is no branch, so that root is refused `bump-leg-root-detached`. 2. **The push line names the branch on both sides.** NEXT ended `push -u origin <branch>`. A bare branch name is a refspec, and a leading `+` means force. - Measured on main: a root on `+x` was told `push -u origin +x`. Pasted, it printed `+ e124684...c8932ad x -> x (forced update)` and never pushed `+x`. - On `+main`, pasted, it printed `+ a6b763e...c8932ad main -> main (forced update)`: origin's default branch was rewound. - Now the line is `push -u origin refs/heads/<b>:refs/heads/<b>`. Measured: `+x` and `+main` push as new branches and leave origin's `x` and `main` where they were. `v1` beside a tag `v1` pushes too, where git's own bare `v1` fails with `src refspec v1 matches more than one`. `-u` still records `origin/<b>`. 3. **`bump-leg-root-dirty` spells each name.** Under `core.quotePath=false` git prints a file name as its raw bytes. On main a tracked, edited `caf<U+009B><U+202E>evil.yml` went to stderr as the raw bytes `302 233 342 200 256`. - Each listed line now goes through `repo_shape.spelled`, so this one shows as `caf\x9b\u202eevil.yml`. - The answer is now split on `\n` only. `splitlines()` also broke a name at U+0085 and U+2028, and `strip()` dropped one from the end of the last name. 4. **`committed` lists what the commit holds, and no `X -> X`.** - The line printed the staged list, and the leg is always staged. Measured on main: after a person moved the gitlink by hand and committed, it printed `committed ...: spec, contracts/spec-pin.yaml` while the commit held only the pin. The report and the commit message both said `52b767b3490a -> 52b767b3490a`. - Now the paths come from `git diff-tree --no-commit-id --name-only -r -z --ignore-submodules=none <sha>`. Each name is passed through `spelled`, like the dirty list. - Where the gitlink already holds the new commit, the `commit` line and the message name the pin's own `commit:` as the old commit. A `gitlink` line, and a sentence in the message, say the gitlink does not move. - `was` is still what the rollback restores. - The listing runs after the commit has landed, so a failure there prints a sentence pointing at `git show --name-only --ignore-submodules=none <sha>` (since the fix round) and the run still exits 0. Before, a `git-failed` refusal would have exited 2 and implied the bump never landed. 5. **The dry run says it runs no validator.** Its closing paragraph now says that a dry run does not run `scripts/validate-pins.py` or `scripts/validate-manifest.py`, and that the real run can still end `bump-leg-validators-red`, rolled back. Behaviour is unchanged, as the issue recommends. The `cmd_bump` docstring said the dry run refuses everything the real run refuses; it now names this one exception. Also: the `BRANCH_LEFT` comment said "the three tools follow one rule". It now names the four copies: `family.py`, the holder's `validate-family.py`, `setup-project.py` and this one (rv647's nit 3). - **The push form is `refs/heads/<b>:refs/heads/<b>`, not `refs/heads/<b>` alone.** The issue allows either. Lane 4's branches print the both-sides form: `fix/689-next-push-refspec` (`fa513848`) for `family.py`'s NEXT, and `fix/674-update-shape-apply` (`d0699029`) through `_full_refspec`. The brief asks this PR to match their form, so all three tools print one form. Both forms were measured to push `+x`, `+main` and `v1` correctly and to set the upstream. - **`diff-tree` rather than `git show` for the `committed` list.** Measured on git 2.43: - `git show --name-only` drops the gitlink under `diff.ignoreSubmodules=all`. `diff-tree` is plumbing and ignores that setting. - Plain `diff-tree` also drops the gitlink under `submodule.spec.ignore=all`. `--ignore-submodules=none` overrides it. - The bump itself still lands under both settings. - **The ride-along (the root's validator environment) is left as it is.** The issue gives the choice. Measured on both trees, with the root's `scripts/repo_shape.py` deleted and committed: - With `PYTHONPATH=<standard>/scripts`, the gate prints `FINDING shape-copy-missing: ... scripts/repo_shape.py is pinned but absent`. - Without it, the gate prints `ModuleNotFoundError`. - Both runs exit 2 and roll back. No verdict changes, and `-S` with no `PYTHONPATH` would only swap the precise finding for a traceback. - **README.md:527 and AGENTS.md:339 are unchanged.** They are placeholder recipes, `git -C <path> push -u origin <branch>`, right after the `switch -c <branch>` the person types. They stay true for the names people choose, no test ties them to the tool's printed line, and the README stays out of the README queue. - **`docs/release-notes/v1.0.0.md` lines 433 and 440** list two of these lines as v1.0.0 known limits. They describe v1.0.0 and are left alone; the v1.1.0 notes are lane 3's. - **No `--help` text changed**, so `docs/cli.md` is unchanged, and `tests/test_cli_reference.py` passes. New file `tests/test_bump_leg_branch_and_push_line.py`, 20 tests in the first round and 24 after the fix round (see Fix round): - **Branch:** a tag `main` beside the tracking branch; a HEAD on a ref outside `refs/heads/` (`refs/tags/release-1` and `refs/remotes/origin/main` since the fix round). - **Push:** `+x` and `+main` with origin's other branch moved on, each run as the printed argv; `v1` beside a tag `v1`, with its upstream checked. - **Dirty list:** four names (U+009B with U+202E, U+2028, U+0085, a name ending in U+2028), each under both the real run and `--dry-run`, plus a default-quoting control. - **`committed` line:** checked plain and under `submodule.spec.ignore=all` and `diff.ignoreSubmodules=all`; the gitlink moved by hand, in both the dry and real runs; the line when git cannot list the paths. - **Dry run:** a pin that `validate-pins.py` already refuses. Tests that held the old form were updated: - `tests/test_bump_leg.py::test_the_message_and_the_next_step_are_a_pull_request` - `tests/test_bump_leg_paste_lines.py`: `test_the_closing_next_is_one_command` (3 params), `test_an_ordinary_root_prints_the_line_it_always_did` and `test_the_closing_next_carries_the_branch_whole` (3 params). Its paste tests still run the line through `sh` and check the remote. Results: - **On main `35133f29`:** the new file gives `19 failed, 1 passed`. The one pass is the default-quoting control, which is meant to pass there. - **The bump-leg set on this head `326de720`:** every `tests/test_bump_leg*.py`, plus `tests/test_branch_left_bump_leg_setup.py` and `tests/test_non_utf8_git_reads.py`. Result: `277 passed, 1 skipped`. - **Every other test file that imports or drives `bump-leg.py`:** `tests/test_bootstrap_after_bump.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_doctor_gitlink_repairs.py`, `test_doctor_pin_restore_merges.py`, `test_doctor_unfinished_merge.py`, `test_family_switch_branch_exists.py`, `test_pin_rewrite_layout.py`, `test_stdout_encoding.py`, `test_template_text_placeholders.py`, `test_update_shape_paste_lines.py`, `test_validate_pins_lockstep_lines.py`, `test_validate_pins_unreadable_workflows_dir.py` and `test_validator_child_encoding.py`. Result: `697 passed, 1 skipped`. - That run started a minute before the last code edit, so the three files among them that read a NEXT or push line were run again on `326de720`: `217 passed`. - **The two skips** are only for other platforms: one needs a case-ignoring disk, the other is about Windows' default encoding. One mutant of `bump-leg.py` per hunk. Each was run against the new file, and the two push mutants also against the paste-line tests. All 12 fail at least one test: | Mutant | Tests failed | |---|---| | `--short` restored | 3: the tracking-tag, tag-ref and `v1` tests | | dirty list printed raw | 8 | | `spelled` kept but `splitlines()` restored | 6: the U+2028 and U+0085 names | | the old commit is always `was` | 1 | | no `gitlink` line | 1 | | message uses `was` | 1 | | staged list printed | 5 | | no guard after the commit | 1 | | no `--ignore-submodules=none` | 1 | | dry-run sentence removed | 1 | | bare branch in NEXT | 12 | | one-sided `refs/heads/<b>` in NEXT | 12 | The comment and docstring hunks change no behaviour and have no mutant. `git merge-tree --write-tree` of `326de720` is clean against: - origin/main `de926142`, and `2fac9790`; - lane 4's `fix/674-update-shape-apply` (`d0699029`) and `fix/689-next-push-refspec` (`fa513848`); - lane 5's `fix/665-four-guards` (`151a0861`), which also edits `tests/test_bump_leg_paste_lines.py`. Main up to `2fac9790` changes `repo_shape.git_out`'s refusal text (#610), but not `spelled` or `pin_values_in`, which this PR imports. - No platform-specific behaviour was measured on Windows or macOS. The new tests run each printed line as the argv a POSIX shell reads from it, without a shell, and skip a file name the disk cannot hold. - rv672's checklist candidates 1-3 (see Fix round) stay checklist candidates; this PR fixes none of them. One new commit, `a5d473ee`, on top of `326de720`. - **S1:** the test of a HEAD outside `refs/heads/` is now parametrized over `refs/tags/release-1` and `refs/remotes/origin/main`. With `refs/tags/x` it passed by coincidence even without the `refs/heads/` guard, because `refs/tags/x` is as long as `refs/heads/` and the leftover name was empty. rv672's mutant R1 (the guard removed) now fails both cases. - **S2:** `current_branch` takes only git's newline off `symbolic-ref`'s answer (`rstrip("\n")`). `strip()` also took a trailing U+00A0, U+2028 or U+0085 off the branch name. So a root on `feat<U+00A0>` beside a branch `feat` committed on the first and printed a push of the second. A new test puts `feat<U+00A0>` beside `feat` and runs the printed line: the bump commit reaches origin as `feat<U+00A0>`, and `feat` is not pushed. - **N1:** `_ensure_root_clean` takes off only the last newline. The first line keeps git's leading space, so ` M` no longer reads as `M` (staged) one column left of the lines under it. The dirty-list expectations in the new file and in `tests/test_non_utf8_git_reads.py` now read ` M ...`, and a new test with two dirty files checks that both lines line up. - **N2:** the `committed` fallback names `git show --name-only --ignore-submodules=none <sha>`. - **N3:** where the gitlink already recorded the commit, the commit message no longer says the gitlink moves in that commit. It says the pin and the workflow references are brought to the gitlink in ONE commit, because `scripts/validate-pins.py` refuses when they disagree. The ordinary message is byte for byte what it was (checked by rendering both). - **N4:** a new test moves the gitlink by hand with the pin's `commit:` set to `not-a-sha`. The `commit` line then names the gitlink's commit, never that text. rv672's mutant R2 (no `COMMIT_RE` check) now fails it. - **B1 and the body:** this draft holds no raw control or bidi character. Every escape in it is ASCII text. - **rv672's checklist candidates 1-3** stay checklist candidates and are not part of this PR. Fix-round tests, on the tree `a5d473ee` commits: - The new file: `24 passed in 19.33s`. - The bump-leg set: every `tests/test_bump_leg*.py`, plus `tests/test_branch_left_bump_leg_setup.py` and `tests/test_non_utf8_git_reads.py`. Result: `281 passed, 1 skipped in 252.20s`. The skip is the case-ignoring-disk test. Fix-round mutants: all 18 of the final head fail at least one test. That is the 12 above, re-made on this head, plus R1, R2 and one each for S2, N1, N2 and N3: | Mutant | Tests failed | |---|---| | R1, no `refs/heads/` guard | 2 | | R2, `_moved_from` unchecked | 1 | | S2 reverted (`strip()`) | 1 | | N1 reverted (`strip(" \n")`) | 10 | | N2 reverted | 1 | | N3 reverted | 1 | `git merge-tree --write-tree` of `a5d473ee` is clean against: - origin/main `5c990708`, and `2d988200` after it moved (rv672's verify round: clean, and the bump-leg set gives 331 passed, 1 skipped on the merge); - `fix/674-update-shape-apply` (`d0699029`); - `fix/689-next-push-refspec` (`fa513848`); - `fix/665-four-guards` (`151a0861`). **Review.** rv672 re-verified the fix round: APPROVE WITH NITS at `a5d473ee`, the nits being these body lines, now fixed. R1 fails both new cases and R2 fails the `not-a-sha` test; the bump-leg set gives 331 passed, 1 skipped on the head and on its merge with `2d988200`. Review: rv672, lane 5's independent review, approved with nits; w672 took S1, S2 and N1-N4, and rv672's verify approved a5d473e with nits, with the body nits fixed. CI (Brett Heap's ruling 27): H0 a5d473e passed tests, tests-git-bash and SonarCloud; the head a5139a3 merges #723's final head and then main after #723 landed as bad05d2, taking the branch's own side for #723's files, with tests, tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree bb920ac over main bad05d2: the bump_leg, bump_leg_branch_and_push_line, bump_leg_paste_lines, non_utf8_git_reads, repo_hygiene, agents_md_codes, cli_reference sets, 310 passed, 1 skipped. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts". Closes #672 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_utf8_git_reads.py imports both sides' conftest names, so #723 and #746 land under #758 with `appended_to_command_scope` still in place (#758) #723 (`bad05d21`) and #746 (`fc867207`) landed on main after `eaa72d9e`. The merge conflicts in one place: the `from conftest import` block of `tests/test_non_utf8_git_reads.py`. #758 added `appended_to_command_scope` there, and #723 added `skip_message`, `skip_where_fsencode_gives_a_surrogate_no_byte` and `windows_fsencode`. The resolution keeps both sides' names, sorted as the file sorts them: from conftest import (ORG, PROJECT, REPO, appended_to_command_scope, cloned_just_pushed, git, run_script, skip_message, skip_where_fsencode_gives_a_surrogate_no_byte, windows_fsencode) No other file conflicts. The scan in `tests/test_no_auto_maintenance.py` flags no file on the merged tree, and no file outside that test's own docstring uses `PASTE_ENV` or `ALLOW_FILE_PROTOCOL`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
main gained #748 (#662, `431c8cf8`), #731 (#692, `d66a9054`), #810 (#674, `d17564d9`), #809 (#688, `42aed788`), #816 (refs #684, `fa04d2cd`), #813 (#689, `f6d17bc5`) and #723 (#663, `bad05d21`) since 64ec47c. No conflict, and none of them touches README.md, the handbook, AGENTS.md, docs/ or the README cap's ledger, so every handbook marker reads as before. README.md is 1474 lines, its cap; AGENTS.md 494, its cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#813 and #723, v1.0.0's quoted-header and plain-path-clone limits closed and the counts current to main bad05d2 (#563) Each entry is the one RV630 corrected in its pre-review of the B2 stage, put at its anchor, and checked against its squash on main: the `git merge-tree --write-tree` of main before the squash with the head the entry was written from equals the squash's tree for all seven. - #810 (#674, `d17564d9`): a new bullet, "`apply`'s lines, its branch and its headers.", after "Headers, branches and messages.". It closes v1.0.0's quoted `files:`/`shape:` header (v1.0.0.md:439) and `apply`'s half of the paths listed as staged (:433); `bump-leg.py`'s half waits for #746. - #731 (#692, `d66a9054`): appended to "Folders, asked before they are made.". - #813 (#689, `f6d17bc5`): appended to "`--tracking-branch`.". - #809 (#688, `42aed788`): a new bullet, "What `family.py init` records and commits as.", after "Names that are not UTF-8.". - #816 (part of #684, `fa04d2cd`): a new bullet, "More holder states, named.", after "family.py refuses before it writes.". #816 closes no issue; #684 keeps its reftable case and is on v1.1.1. - #748 (#662, `431c8cf8`), then #723 (#663, `bad05d21`): in the CI bullet, after #713's sentence. With #554 and #578, #748 closes v1.0.0's limit on fixtures that clone a just-pushed remote by plain path (:530). The counts, in the header comment, the counts paragraph, "What changed" and the open list: main `bad05d2`; 120 pull requests since `bfed143`; at 18:02Z, 3 of the `v1.1.0` milestone's 133 issues open and 130 closed. The open list is #563 (#630), #672 (#746) and #686 (#795); #684 and #695 (#738) are on v1.1.1. #746 and #795 have not landed, so neither has an entry yet. Nothing since 2000488 touches pyproject.toml, src/openreposhape/, install.sh or .github/workflows/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it closed as one clause, the list's and the known limits' markers made final, the triage lines named by their issues, and the counts current to main fc86720 (#563) - #746 (#672, `fc867207`): a new bullet, "`bump-leg.py`'s branch and its lines.", after "`bump-leg.py --dry-run` gives the real run's answer.". `git merge-tree --write-tree bad05d2 a5139a3`, the head #746 was gated and landed on, equals the squash's tree `bb920ac1`. The head the entry was written from, `4a014a79`, sat on #723's head before its squash and does not merge clean with main; `scripts/bump-leg.py` is one blob (`226e7b0d`) at `4a014a79`, `a5d473ee`, `a5139a32` and the squash, and the squash's own lines in each of its five files equal that head's, so the entry stands as written. - v1.0.0's limit on the paths listed as staged (v1.0.0.md:433) is one clause, in #746's entry: with #810 it closes. #810's half-clause is cut. `bump-leg.py --dry-run` over a pin its validators refuse (:440) stays a limit, which the dry run now says. - "What changed" runs to main `fc86720`, and says #795 is added if it lands next; Known limits' re-read marker and "as read so far" are cut. The counts marker stays as the release-body tool reads it. - Known limits name each triage by its issue: "the checklist triage of the L4 lines on #457 and #459" and "of the L1 lines on #457", where they named lanes 4 and 1 and D9, and each id reads "#457's L4-05", "#459's L4-24" and so on. - Counts in the four places: main `fc86720`; 121 pull requests since `bfed143`; at 19:08Z, 131 of the `v1.1.0` milestone's 133 issues closed, the open two #563 (#630) and #686 (#795). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd what they tell git to the `GIT_CONFIG_COUNT` entries the environment holds, so a pasted line keeps the session's maintenance settings and the Windows job's two (#758) (#818) Against main. #748 landed as `431c8cf8`; this branch merged #748's final head and main (see Fix round 1). After that merge, #723 (`bad05d21`) and #746 (`fc867207`) landed. The branch is refreshed over main `fc867207` as `4a553e17`, one merge commit. Its only conflict was `tests/test_non_utf8_git_reads.py`'s `from conftest import` block, resolved by keeping both sides' names, as rv818 asked. The merged tree differs from main in #818's own 27 files, and #818's count scan flags no file. Tests only: 25 files under `tests/`, no shipped code, no pinned file, no README, handbook, `AGENTS.md` or `docs/cli.md` change. Two new commits on `b6e15591`: the retarget merge `2908e46f`, then the fix commit `eaa72d9e`. Tests only, as before. ### The retarget: merge commit `2908e46f` #748 landed as `431c8cf8` from its final head `8368d1f8`, and GitHub moved this PR's base to main. The merge commit has three parents: `b6e15591`, `8368d1f8` and main `fa04d2cd`. #748's head goes in first, so git sees the history the squash dropped. - **#748's head did not merge clean**, as rv818 found. `tests/test_adopt_empty_symlinks.py` conflicts, because `8368d1f8` holds main `741679e7`, which carries #388. - I took w758's earlier resolution of that file, which rv818 checked. Main's side of the file is the same at `974d4827`, `8368d1f8` and `fa04d2cd`, so it still fits. - Both sides stay: #388's two targets (`with_each_target`, `SAID`, the `target` parameters) and #758's `symlinks_off()`, built at the call. - `test_the_link_given_a_target_adopts_end_to_end` calls `symlinks_off()` too. Git does not flag it, and left as it was the file fails at collection. - **Main then conflicted in three files** (main was `d17564d9` when I resolved them): `tests/test_clone_just_pushed.py`, `tests/test_doctor_legs_members_next.py` and `tests/test_family_bump_remove_rollback.py`. - Main's copy of each was `8368d1f8`'s byte for byte. - This branch's copy is `8368d1f8`'s plus #758's own hunks, so this branch's side is taken. - Main's next two landings, #809 and #816, merged with no conflict. - **rv818 MUST 1.** `tests/test_local_remote_dir_urls.py` came with #812 and imported `ALLOW_FILE_PROTOCOL` from `test_family`. - Without a change, the file fails at collection: `ImportError: cannot import name 'ALLOW_FILE_PROTOCOL' from 'test_family'`, and none of its 27 cases run. - It now imports `allow_file_protocol` and calls `pasted(lines, paste, allow_file_protocol())`. - `git grep` finds no other file on main that uses a name #758 removes (`SYMLINKS_OFF`, `SYMLINKS_ON`, `CONFIG_COUNT`, `CONFIG_VALUE`, `PASTE_ENV`, `ALLOW_FILE_PROTOCOL`). `NO_MAINTENANCE`, `QUOTE_PATH_OFF` and `tool_env` change shape, and each is used only in its own file. - **Checked:** - Against main, the merged tree differs in #758's 25 files and in `tests/test_local_remote_dir_urls.py`. - Each of the 25 differs by exactly #758's own added and removed lines, `tests/test_adopt_empty_symlinks.py` aside. - Collecting the whole suite at the head finds 12,320 tests and no error. ### The fix commit `eaa72d9e` - **SHOULD 2:** `configure` in `tests/test_adopt_reencoded_files.py`, the seventh append from #758's line 2, now sets what `appended_to_command_scope(os.environ, pairs)` returns. It gives the same entries in the same order, so no test can fail on the old form. - **SHOULD 3:** the scan also reads a name the module binds to `"GIT_CONFIG_COUNT"` (`CONFIG_COUNT = "GIT_CONFIG_COUNT"`, annotated or not) as the count. It does so in a dict key, a setter's first argument and a subscript. - rv818's repro: append the base's two lines to `tests/test_adopt_root_symlinks.py`. - The old scan passes. The new one names line 2137. - **SHOULD 4:** `counts_set_to_a_number` is split into one reader per node type: `in_a_dict`, `in_a_call` and `by_subscript`. The walk takes each node's reader from `READERS`. By hand, no function scores more than 5 on S3776. I could not run Sonar. - **NIT 5:** `conftest.py` is no longer skipped. It scans clean, at the head and on today's main. - **NIT 6:** - New "sees" cases: `os.putenv("GIT_CONFIG_COUNT", "1")` and a bare `setenv("GIT_CONFIG_COUNT", "1")`. - The walk is now `files_off_their_count(directory)`. A new test runs it over a folder of its own, and checks three things: - `conftest.py` and a helper module are read; - a `.txt` file is not; - a file `OWN_COUNTS` allows is named for fewer counts as well as for more. - Four of rv818's six surviving mutants now fail a case: `putenv` dropped, the bare-name setter dropped, `!=` changed to `>`, and `glob("test_*.py")`. - The fifth, `conftest.py` scanned, is now how the code works, and putting the skip back fails the new test. - The sixth, the count's value taking any string, still survives: it is a stricter scan. Git refuses a count that is not a number (`bogus count in GIT_CONFIG_COUNT`, exit 128), so a test that holds one fails on its own. - **NIT 7, in part:** - An empty count (`""`) is now a count of its own. Git 2.43 and 2.47.1 read it as no entries at all. Measured: `GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0=a.b GIT_CONFIG_VALUE_0=c git config --get a.b` exits 1. - The docstring says the scan reads the count's value as a constant only. - Left out: - a value built by a call (`str(len(pairs))`) or held in a variable; - an int; - `dict()`, or `environ={}` by keyword, handed to the helper; - `setenv` with keyword arguments; - an annotated subscript. rv818 judged only `str(len(entries))` likely, and telling it from a count worked out needs what the call means, not how it is spelled. - **NIT 8:** the paste case deletes `GIT_CONFIG_PARAMETERS` first. Measured on the merge before the fix commit, with `GIT_CONFIG_PARAMETERS="'probe.param'='p'"`, the case failed (`Left contains one more item: 'probe.param=p'`). It passes now. - **NIT 9, left as it is:** main's three appends built by hand each work the count out right: `tests/test_family_head_lost_gitmodules.py:97`, `tests/test_family_bootstrap_gate_order.py:135` and `tests/test_family_add_spellings_remedies.py:137`. Each of those files is also changed by another open PR (#828 `fix/761-add-move-back-gitmodules`, #820 `fix/762-validate-family-remedies-texts`, `fix/763-holder-docs-agree`), so converting them here would only add conflicts. - **NIT 10:** the comment on `NO_MAINTENANCE` now says its `maintenance.auto` and `gc.auto` repeat the session's two, which reach these gits in the command scope. Only `gc.autoDetach` adds a setting. The two stay, so the constant alone still keeps maintenance off for these gits. - **NIT 11:** the count was in lane 5's #473 summary, not in this body. The seven sets held 2,401 distinct passes, not 2,609: the 208-test run repeats three files of the 256-test run. ### Tests at `eaa72d9e` All runs went through `run9.sh`, with the real `gh` unreachable. All passed, none skipped: - **The 26 test files the PR changes against main**, in one full-file run each: 2,206 passed (1,108 + 1,098). - These are #758's 24 test files, `tests/test_local_remote_dir_urls.py` and `tests/test_adopt_reencoded_files.py`. - The count matches the collection exactly. - **The 43 files that import any of those 26, with `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`:** 1,646 passed (825 + 821). - **The same merge made on main `d17564d9`** (a local commit, `c16a7e1a`, never pushed), before the fix commit: `tests/test_local_remote_dir_urls.py` and `tests/test_no_auto_maintenance.py`, 55 passed. ### Fails on the base The new tests were run against the base's scan and walk, as `b6e15591` has them, with `GIT_CONFIG_PARAMETERS` set: 6 failed, 30 passed. The six: - the paste case; - the `nothing`, `a-name-bound-to-it`, `an-annotated-name` and `a-name-by-subscript` cases; - the walk test. The three other new cases (`a-bare-setenv`, `putenv`, `a-name-bound-to-another-key`) are there to kill mutants, and they pass on any correct scan. MUST 1 fails on main's form at collection, as above. SHOULD 2 and NIT 10 change no behaviour. ### Mutants I made 26 mutants. 25 are caught, and one survives as rv818 said it would: - **22 on the scan and the paste case**, each run against `tests/test_no_auto_maintenance.py`: - the delenv dropped; - `putenv` dropped from `SETTERS`, or the bare-name setter; - the name branch of `is_the_count` dropped, or any name taken as the count; - `""` no longer a count; - the `Assign` names dropped, or the `AnnAssign` names; - no names collected; - each reader ignoring the names; - each of the three readers dropped from `READERS`; - `conftest.py` skipped; - `!=` changed to `>` or to `<`; - `glob("test_*.py")`, or `glob("*")`; - `by_subscript` taking any value; - the walk pointed at an empty folder. The last passes on a clean tree, like any change to an assertion that nothing is found. It is caught by rv818's SHOULD 3 repro, where the real walk fails and the mutant passes. - **The survivor:** the count's value taking any string. It makes the scan stricter (see NIT 6). - **`configure`:** with `appended_to_command_scope({}, pairs)`, the scan names the file. With the pairs dropped, three of its tests fail. - **MUST 1:** main's import put back fails collection with `ImportError`. ### Merge checks at `eaa72d9e` I ran `git merge-tree --write-tree` against `origin/main` (`fa04d2cd`) and the 53 other `fix/*` and `l3-*` heads on origin: - **Clean:** `origin/main` and 43 heads. - **#792 (`l3-785-non-ascii-paths`)** merges clean, but `tests/test_doctor_non_ascii_paths.py` imports `PASTE_ENV`. Whichever PR lands second changes `:50` (the import), the `:266` docstring and `:268` to `paste_env()`. - **#723 (`fix/663-non-utf8-name-tests`), #746 (`fix/672-bump-leg-branch-and-push-line`, which now holds #723) and #806 (`fix/757-non-utf8-test-dirs`)** conflict only in the `from conftest import` block of `tests/test_non_utf8_git_reads.py` (and of `tests/test_adopt_git_reads.py`, for #806). Both sides add names, so keep them all. - **Seven heads conflict with main itself, not with this PR:** - `l3-732-latin1-section` and `l3-733-ignored-sections` (`tests/test_doctor_legs_members_next.py`); - `fix/712-validate-pins-contracts`, `fix/766-spelled-paths` and `fix/798-adopt-legs-asked-first`; - `l3-677-pins-hand-step-wip` and `l3-678-naming-checkout-wip`. - I scanned each clean merged tree's changed test files for a name #758 removes, or a count the scan flags. Only #792 shows up. - **Main moved after the push.** #723 landed as `bad05d21`, and the two now conflict in one place: the `from conftest import` block of `tests/test_non_utf8_git_reads.py`. The fix is to keep both sides' names: `ORG, PROJECT, REPO, appended_to_command_scope, cloned_just_pushed, git, run_script, skip_message, skip_where_fsencode_gives_a_surrogate_no_byte, windows_fsencode`. Every other change merges in. On that merged tree, with the block resolved that way, the scan flags no file, and no file uses a name #758 removes. On `deb029c9` a probe test imported `PASTE_ENV` and pasted `git config --show-origin --list` through `paste()`: - `PASTE_ENV` held `GIT_CONFIG_COUNT=1` (`protocol.file.allow`), a stale `GIT_CONFIG_KEY_1=user.email` left from collection, and no `GIT_CONFIG_GLOBAL`. - `os.environ` at the call held a count of 4 and the session's global file. - The pasted git listed one command-line entry, `protocol.file.allow=always`, and `git config --get maintenance.auto` exited 1. So a pasted line ran with git's automatic maintenance on, in both scopes, as the issue said. `paste_env()` replaces it. It reads `os.environ` at each call (minus `PYTHONPATH`), adds the identity and `PYTHONDONTWRITEBYTECODE`, and appends `protocol.file.allow` with `appended_to_command_scope`. The six uses call it. The import-time copy also predated the session's `gh` guard on `PATH`, as `test_doctor_next_is_a_command.py`'s docstring says of its own environment; a pasted line now gets the `PATH` of the call. That docstring now says `paste_env()` reads at the call too. - **The seven.** Six are on this base, each now one call to `appended_to_command_scope`: `test_adopt_git_reads.py`, `test_adopt_nested_attributes.py`, `test_adopt_ignored_shape_files.py`, `test_adopt_root_symlinks.py`, `test_bootstrap_after_bump.py`, `test_family_bump_remove_rollback.py`. The seventh, `test_adopt_reencoded_files.py`, came with #743 and is converted in fix round 1. `test_clone_just_pushed.py:96` stays, as the issue says: it is the helper's own test. - **The other literals.** Each sets a count of its own and is laid over `os.environ`, where the session's two settings and the Windows job's two sit, so each replaced them (`NO_MAINTENANCE` put the two maintenance keys back and dropped the job's two). I read each; each is converted, none left: - `test_family.py`: `ALLOW_FILE_PROTOCOL` is now `allow_file_protocol()`; its two importers (`test_siblings_family_name.py`, `test_family_siblings_placement.py`) and the four uses in `test_family.py` call it. - `test_family_dry_run.py`: `NO_MAINTENANCE` is a tuple of entries, appended by `isolated()`. - `test_non_utf8_git_reads.py`: `QUOTE_PATH_OFF` is a tuple of entries, which `tool_env()` appends; `user_name()` appends too. - `test_adopt_empty_symlinks.py`: `SYMLINKS_OFF` is `symlinks_off()`. - `test_adopt_root_symlinks.py`: `SYMLINKS_OFF` and `SYMLINKS_ON` are gone; the four tests that set the first call `links_as_files` and the two that set the second call `told_git`, which already appended. - one-off environments: `test_scaffold_reuse_seeded_legs.py`, `test_adopt_split_branch.py`, `test_scaffold_free_text_values.py`, `test_adopt_plan_yaml_quoting.py`, `test_setup_project_py.py`, `test_setup_sh.py`, `test_doctor_next_is_a_command.py`. - The two tests that build a held environment on purpose to see it kept (`test_adopt_git_reads.py`, `test_clone_just_pushed.py`) now build it with `appended_to_command_scope({}, ...)`. Measured with a probe on a throwaway bare remote after a push (18 loose objects): the hand-built options and `clone_just_pushed`'s give the same clone, one pack and no loose object, with `upload-pack` and `pack-objects` in `GIT_TRACE`, and `origin` keeps the plain path. A plain local clone of the same remote keeps all 18 loose objects. For a clone that is not recursive the helper's `insteadOf` rewrite is inert, because no submodule is cloned; the helper adds one place for the options, not a different read. - `test_bump_leg_dry_run_truth.py:98`, the private bare copy of the session's spec remote: a session remote. `cloned_just_pushed(..., bare=True)`; the fixture now asserts the copy is bare. - `test_bump_leg_dry_run_truth.py:110`, `private_work`, a clone of that private remote, which tests push tags into: `cloned_just_pushed(..., recurse=False)`. - `test_doctor_legs_members_next.py:1828` and `:1929`, `Ghost`, a clone of the first member's checkout in the test's own holder copy: not a remote, and the hand-built `-c protocol.file.allow=always` was dead weight on a direct clone. Both call `clone_member`, one helper over `cloned_just_pushed(..., recurse=False)` that asserts the legs stay empty. - `test_family_bump_to_checks.py:235`, a clone of `a_private_remote`, a bare repository filled by a push: `cloned_just_pushed(remote, work, "main", recurse=False, env=env)`. - `test_doctor_pin_restore_merges.py:714` is **left as it is**: a `--filter=blob:none` clone of a file URI. The helper takes a path, and has no way to pass `--filter`. - `test_family_dry_run.py`: the comment on `NO_MAINTENANCE` says what the entries are for, the gits `isolated()` hands an environment to, whose global file `excluding()` replaces, and that the session fixture covers the module's own setup commits, which pass no `env=`. - `tests/conftest.py`: the two i645 replacements, "repacks once its estimate, taken from `objects/17` alone, is above 256 loose objects, so two loose objects there are enough", and the `objects changed` sentence. **#713's body holds the matching sentence and needs the same two replacements; I cannot edit a body.** `tests/test_no_auto_maintenance.py` has 17 new cases (28 in the file, 11 before): - `test_a_pasted_line_keeps_the_entries_the_environment_holds_at_the_call`: the environment holds two entries as the Windows job does, then the two settings as the session fixture appends them, set after the doctor tests were imported. `git config --show-origin --list` through `paste()` lists the four, in order, then `protocol.file.allow`, and reads both settings from the session's global file. - `test_no_test_sets_a_git_config_count_of_its_own`: reads every `tests/*.py` but `conftest.py` with `ast` and fails a `GIT_CONFIG_COUNT` set to a number (dict, keyword, `setenv`/`setdefault`/`putenv`, subscript) and an `appended_to_command_scope` call handed a dict written out. Three files are allowed the ones that build a held environment on purpose, with a count each in `OWN_COUNTS`. - 15 cases that hold the scan itself: seven spellings it must see, eight it must leave (`env.get("GIT_CONFIG_COUNT", "0")` is a read). Runs at the head `b6e15591` through `run9.sh` (the real `gh` unreachable), all passed, none skipped: - `test_no_auto_maintenance.py`, `test_doctor_legs_members_next.py`, `test_doctor_next_is_a_command.py`: 208 passed. - `test_clone_just_pushed.py` and `test_doctor_validator_rows.py` (it imports the doctor file), with those three: 256 passed, run one commit earlier (the later commit only moved two blank lines and reworded one failure message). - The sweep files: `test_adopt_git_reads.py`, `test_adopt_nested_attributes.py`, `test_adopt_root_symlinks.py`, `test_adopt_ignored_shape_files.py`, `test_bootstrap_after_bump.py`, `test_family_bump_remove_rollback.py`, `test_adopt_empty_symlinks.py`, `test_adopt_split_branch.py`: 445 passed. - `test_non_utf8_git_reads.py`, `test_family.py`, `test_siblings_family_name.py`, `test_family_siblings_placement.py`, `test_scaffold_reuse_seeded_legs.py`, `test_scaffold_free_text_values.py`, `test_adopt_plan_yaml_quoting.py`: 999 passed. - `test_family_dry_run.py`, the five files that import its `isolated` (`test_family_bump_to_a_tag.py`, `test_family_bump_to_checks.py`, `test_family_bump_to_local_only.py`, `test_family_refuse_up_front.py`, `test_family_remove_gitmodules.py`) and `test_bump_leg_dry_run_truth.py`: 216 passed. - `test_setup_project_py.py`, `test_setup_sh.py` and their importers (`test_adopt_shape_pin_and_work_dir.py`, `test_dry_run_asks_remotes.py`, `test_clone_url_bytes.py`, `test_setup_by_hand_commands.py`): 313 passed. - `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_doctor_pin_restore_merges.py`, `test_git_helper_identity.py`: 172 passed. `render-cli-reference.py --check` is ok. **On the base (`deb029c9`)**, with the new `test_no_auto_maintenance.py` copied in: 2 of the 17 fail, the paste case and the scan (it finds 12 files). The other 15 hold the scan itself and pass on any tree. Each of the new hunks was broken in a throwaway and run against its tests: - `paste_env`: a literal count of 1, a snapshot taken at import, and `appended_to_command_scope({}, ...)`: each fails the paste case, and the first and third the scan too. - The 18 sites that call `appended_to_command_scope(os.environ, ...)` (or `env`), each with `{}` in place of the environment: each fails the scan. Sites: the six of line 2, `allow_file_protocol`, `isolated`, `tool_env`, `user_name` (non-UTF-8), `symlinks_off`, `git_reads_github_from`, `github_at`, `user_name` (free text), `test_adopt_plan_yaml_quoting.py`, the two setup tests, `test_doctor_next_is_a_command.py`. - The scan: eight changes (ignore dicts, keywords, `setenv` calls, subscripts, `appended_to_command_scope({}`; flag `os.environ`; flag a read; allow fewer in `OWN_COUNTS`) each fail a scan case. - The clone sites: dropping `bare=True` from the private remote, `recurse=False` from `clone_member`, and `"main"` from the checks clone each fail. Two **survive as equivalents**: `recurse=False` in `private_work` (the spec leg has no `.gitmodules`, so a recursive clone of it is the same clone) and `env=env` in the checks clone (a clone reads no config the isolated environment changes). - The two comments cannot be mutated. `git merge-tree --write-tree` of this head against `origin/main` (`974d4827`), #748 (`deb029c9`), #713 (`979bb394`) and every `fix/*` and `l3-*` head on origin (46 refs): - **Clean:** #748, #713, and 24 of the 46 heads: 21 `fix/*` heads (every lane-5 PR but #723 and #756, and most of lane 4's) and `l3-677`, `l3-678`, `l3-rehearsal-v1.1.0`. - **`origin/main`: one file, `tests/test_adopt_empty_symlinks.py`** (four hunks), against #388's landing (`5c990708`). The parent #748 merges clean, so this arrives with the retarget. Resolve by keeping main's lines and my `symlinks_off()`. Two more spots need the same change and git does not flag them: - `tests/test_adopt_empty_symlinks.py::test_the_link_given_a_target_adopts_end_to_end` uses `SYMLINKS_OFF`, which this change removes: give it the `off` parametrization. - `tests/test_adopt_reencoded_files.py::configure`, the seventh hand-built append, goes to `appended_to_command_scope(os.environ, pairs)`. - `tests/test_family_head_lost_gitmodules.py::on_disk` (on main) is one more hand-built append: `{**env, **appended_to_command_scope(env, [("core.ignorecase", ignorecase)])}`. The scan does not read for hand-built appends, so it is not required, but it is the same thing. I made that merge in a scratch tree (main at `bf1f099b`), resolved it as above, and ran the files this change touches that main also changed, with `test_adopt_empty_symlinks.py`, `test_adopt_reencoded_files.py`, `test_family_head_lost_gitmodules.py`, `test_no_auto_maintenance.py` and the files that import `isolated` or the doctor helpers: 653 and 245 passed, in two runs. The scan finds no other file of main's with a count set by hand. - **#723 (`fix/663-non-utf8-name-tests`):** `tests/test_non_utf8_git_reads.py`, the `from conftest import` block, where both add names. - **`l3-732`, `l3-733`:** `tests/test_doctor_legs_members_next.py` (the `from conftest import` block) and `tests/test_adopt_empty_symlinks.py`. - **19 more** (`fix/664`, `fix/729` (#756) and 17 `l3-*` heads): `tests/test_adopt_empty_symlinks.py` only, because they carry main's #388 as main does. - **Not a textual conflict, but it breaks on merge:** `l3-785-non-ascii-paths` imports `PASTE_ENV` (`tests/test_doctor_non_ascii_paths.py:47` and `:223`); it must call `paste_env()`. #812 (`fix/693-local-remote-dir`) landed first, with `tests/test_local_remote_dir_urls.py` importing `ALLOW_FILE_PROTOCOL` (`:51`, `:349`). This PR's retarget merge makes it call `allow_file_protocol()` (rv818 MUST 1). - A pasted `git submodule update --init` still clones a local path: `paste()` takes no remotes, so no `insteadOf` rewrite reaches it (rv662's "build it from `read_through_git`"). It runs under the session's maintenance settings now, which is what the issue asks. - Three more appends built by hand beside `appended_to_command_scope`, in files this base does not hold: `tests/test_family_head_lost_gitmodules.py:97` (main), `tests/test_family_bootstrap_gate_order.py:135` (#682), `tests/test_family_add_spellings_remedies.py:137` (#767). The scan reads for counts set to a number, not for appends worked out by hand. Review: lane 2's independent review (issuecomment-6236874291) and its re-verification of eaa72d9, READY (issuecomment-6238781272); its new SHOULD is taken in #848, and 4a553e1 merges main over eaa72d9. CI (Brett Heap's ruling 27): H0 is the head 4a553e1 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base fc86720; and the local gate passed on its predicted squash tree 9d6c099 over main d8b8eb3: the adopt_empty_symlinks, adopt_git_reads, adopt_ignored_shape_files, adopt_nested_attributes, adopt_plan_yaml_quoting, adopt_reencoded_files, adopt_root_symlinks, adopt_split_branch, bootstrap_after_bump, bump_leg_dry_run_truth, clone_just_pushed, doctor_legs_members_next, doctor_next_is_a_command, family, family_bump_remove_rollback, family_bump_to_checks, family_dry_run, family_siblings_placement, local_remote_dir_urls, no_auto_maintenance, non_utf8_git_reads, scaffold_free_text_values, scaffold_reuse_seeded_legs, setup_project_py, setup_sh, siblings_family_name, repo_hygiene, agents_md_codes, cli_reference sets, 2359 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green". Closes #758 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lper whether a name that is not UTF-8 can be made and skip on its two answers only, and three tests skip where an ASCII file name encoding cannot carry their text (#757) (#806) Against main. #723 landed as `bad05d2`, and this branch is restacked onto main as `76ae6419`: it merges #723's final head `4cdb427c`, then main, taking this branch's side in `tests/test_non_utf8_git_reads.py`, the one file #723 also changed. Over main it carries exactly its own change: `git diff 6ba8c5d 6b5e907` and `git diff origin/main 76ae641` add and remove the same lines (7 files, +408/-45). Tests only. No tool, template, pinned file, README, handbook, `AGENTS.md` or `docs/cli.md` changes (`render-cli-reference.py --check`: `[ok] docs/cli.md is what the generator produces`). Seven test files change, one of them new. **No Windows run and no macOS run was made.** CI is Linux only since #597. What Windows and macOS answer is emulated (see the tests below), and the emulation holds the skip decisions, not a real disk. **Line 1: the callers ask the helper, and every other disk error is the test's own.** Four tests and `directory_named` each made the directory themselves and turned any `OSError` into `pytest.skip`, beside `skip_where_a_name_cannot_be_not_utf_8` from #723. That helper skips on the two answers that mean "this system cannot carry the name" (Windows' `os.fsencode` first, macOS's EILSEQ second) and raises the rest. So a full disk or a `tmp_path` this user cannot write in read as SKIPPED in those five and as a failure in the helper's own tests, and no run fails on a skip. - `tests/test_stdout_encoding.py`, `tests/test_adopt_git_reads.py`, `tests/test_adopt_template_values.py` and the directory test of `tests/test_adopt_plan_escapes.py` now call the helper first. The directory they make after the probe (`b"b\xffd"`, `caf` and byte 0xE9) is no longer guarded. - Each test keeps its own second skip: `git will not work under <name>` in the three adopt tests, and `this filesystem rewrote a name that is not UTF-8` in the stdout test. - The file twin, `test_a_leg_extracts_a_path_that_is_not_utf8` in `tests/test_adopt_plan_escapes.py`, asked the same question of a FILE (`caf` and byte 0xE9 plus `.txt`, `write_bytes`, `unlink`). It calls the helper too and no longer writes a probe file of its own. I chose the helper over `make_file`'s skip because `make_file` also skips on any `OSError` of the write (see the candidates), and because the helper's probe is a name that is not UTF-8 under the same directory, which macOS refuses for a file and a directory alike. The file itself is made later, by git, in `filter-repo`'s clone. - `directory_named` in `tests/test_clone_url_bytes.py` is split. `skip_where_this_system_cannot_name(tmp_path, raw)` asks the helper for a name that is not UTF-8 (`raw.decode("utf-8")` fails) and keeps `directory_named`'s own Windows skip for a control character (a byte below 0x20), which Windows refuses in any file name. `directory_named` then makes the directory with an unguarded `os.mkdir`. **The Windows half of #459's line (R433 O5, issuecomment 6214823552), decided with line 1.** The old `directory_named` skipped every row on Windows, the `bidi-override` row (the name U+202E plus `evil`, as UTF-8) included, though NTFS holds U+202E. Now the bidi row is not skipped on Windows: the name is valid UTF-8 and holds no control character. **This is reasoned and emulated, not run.** If a Windows run finds the row's other assertions (a refusal text, `str(bad.resolve())`, `os.listdir`) fail there for a reason that is not a defect, the fix is one more skip in `skip_where_this_system_cannot_name`. **Line 2: the `make_file` test under an ASCII file name encoding.** `test_make_file_writes_a_name_that_is_utf8_where_fsencode_makes_no_byte` asserted `skip_message(make_file, ...) is None`, and `make_file` cannot write `café.yml` where the encoding is ASCII, so the assert failed. The test now asks `skip_where_the_environment_cannot_carry(UTF8)` first, before it swaps in the Windows `os.fsencode`, so it skips for the encoding and for nothing else. The assert is unchanged, so a write refused with EACCES or ENOSPC still fails the test. **Beyond the issue's measured set.** The issue measured `1 failed, 6 passed, 2 skipped` under `PYTHONUTF8=0 LC_ALL=C PYTHONCOERCECLOCALE=0` on the `make_file` tests. Its Verify asks that the WHOLE file `tests/test_non_utf8_git_reads.py` pass under that environment, and on `6ba8c5d2` it gives `6 failed, 40 passed, 6 skipped`. The other five failures hold no defect either: `subprocess` puts each environment value through `os.fsencode`, which raises `UnicodeEncodeError` for `git config user.name` of an accented name, a U+200C name or a U+202E name, in the test and before the tool runs. They are `test_a_user_name_that_is_text_still_initialises[accent-dry-run]`, `[accent-real]`, `[zwnj-dry-run]`, `[zwnj-real]` and `test_every_other_name_no_file_can_hold_is_refused_the_same_way[u202e]`. Both tests now call `skip_where_the_environment_cannot_carry(value)`, which skips where `os.fsencode(value)` raises `UnicodeEncodeError` (the `make_file` test above calls it too, with the file name), and says so as "this system's file name encoding (...) cannot carry '<value>'". ASCII names, and a lone surrogate that `os.fsencode` gives back as its byte, are not skipped. On Windows and under a UTF-8 file name encoding nothing skips that did not before. Lane 1's review of `41849725` asked for three fixes and a nit. All four are in one commit, in `tests/test_non_utf8_git_reads.py` and `tests/test_non_utf8_name_callers.py`. 1. **The `make_file` test skipped on a write the disk refused.** `make_file` skips on any `OSError` of its write with a reason that contains "filesystem", so the first version of the test, which skipped on that reason, ended SKIPPED when `write_bytes` raised EACCES or ENOSPC for the plain UTF-8 name, and the reason it gave was false. The test now asks `skip_where_the_environment_cannot_carry(UTF8)` first and keeps its strict assert. The helper's reason no longer ends "in an environment", and the callers file reads "file name encoding". The ASCII row patches `os.fsencode`, and two rows make `Path.write_bytes` raise EACCES and ENOSPC and expect the test to FAIL. The broader narrowing of `make_file` stays a candidate below. 2. **Five own-directory rows failed on a disk that refuses a name that is not UTF-8** (macOS's APFS, a ZFS with `utf8only`): they let the probe through to the real disk, and the helper skipped. `a_disk_that_takes_the_probe` asks the real disk first and only those rows use it. Measured with a plugin that makes `os.mkdir` answer EILSEQ: `31 passed, 5 skipped` (without the fixture, `5 failed, 31 passed`). 3. **S5778 (two places).** `fail_where_the_caller_ends` holds the `pytest.fail`, so each `pytest.raises` block holds one call, as does the new disk-error row. 4. **The Windows rows' names.** The byte and control-character rows now call `directory_named` itself (chosen over renaming), so its order, asking before it makes anything, is held. The bidi row asks the decision function and is named for it. A new file, so it stays clear of the open branches that edit the five. Each caller is called as a plain function with the disk answering as it would on a system CI does not have: | group | what the disk does | the caller must | tests | |---|---|---|---| | macOS | the probe `probe` + U+DCFF is refused with EILSEQ | skip, on the helper's reason | 6 | | a folder nobody can write in | the probe is refused with EACCES | fail with `PermissionError`, never skip | 6 | | the test's own directory | the probe passes, the test's own directory is refused with EACCES. These rows are skipped where the real disk refuses the probe (EILSEQ): a fixture asks it first | fail with `PermissionError`, where it skipped | 5 + 1 (the fixture asked of a disk made to answer EILSEQ) | | Windows (`os.name` is `nt`, `os.fsencode` gives a surrogate three bytes) | nothing is made | `directory_named` skips for byte 0xE9 (reason names `os.fsencode`) and for a line feed and an escape (names the control character), and does not skip for the bidi override | 4 | | a system that is not Windows | the disk takes it | `directory_named` makes a directory named with a line feed or an escape | 2 | | ASCII file name encoding | `os.fsencode` is ASCII with `surrogateescape` | the `make_file` test and the two environment tests skip, on "file name encoding"; the helper skips no text that is carried | 1 + 3 + 4 + 2 | | a write the disk refuses | `Path.write_bytes` raises EACCES or ENOSPC for the plain UTF-8 name | the `make_file` test still FAILS on its assert, and does not end SKIPPED | 2 | The six callers are the four tests, `directory_named` and the file twin (the last has no directory of its own, so it is not in the own-directory group). The Windows rows for a byte and a control character call `directory_named` itself, so that it is held to asking before it makes anything; the bidi row asks `skip_where_this_system_cannot_name`, because `directory_named` goes on to make a `Path`, which under an emulated `os.name` of `nt` is a `WindowsPath` that Linux cannot make. The "carried" rows (ASCII text, a lone surrogate that is a byte, a line feed, an escape) hold that the environment skip is not wider than it should be. All with a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR` and `/usr/bin/python3`, at head `6b5e9074`. Per file: `test_stdout_encoding.py` 48, `test_adopt_git_reads.py` 21, `test_adopt_template_values.py` 78, `test_adopt_plan_escapes.py` 59, `test_clone_url_bytes.py` 30, `test_non_utf8_git_reads.py` 52, `test_non_utf8_name_helpers.py` 18, `test_non_utf8_name_callers.py` 36, and the four files that import `test_stdout_encoding.py` (`test_bootstrap_branch_bytes.py` 61, `test_bootstrap_spelling.py` 205, `test_siblings_family_name.py` 150, `test_validator_child_encoding.py` 55). - **The twelve files above:** `811 passed, 2 skipped in 295.42s`. Both skips are the same two as on the base and have nothing to do with this change: `test_stdout_encoding.py:285` ("the default this is about is Windows' own") and `test_adopt_template_values.py:224` (`git config user.name` here is not read from `GIT_CONFIG_GLOBAL`). - **The Verify's C locale**, `PYTHONUTF8=0 LC_ALL=C PYTHONCOERCECLOCALE=0`, `tests/test_non_utf8_git_reads.py` and the new file: `74 passed, 14 skipped in 20.01s`. The 14 are the 6 that skipped on the base, the 6 this change turns from failures into skips (the `make_file` test, two accent rows, two U+200C rows and the U+202E row), and the 2 disk-error rows of the new file, which need an encoding that can write the name. On the base the first file gives `6 failed, 40 passed, 6 skipped`. - **A disk that answers EILSEQ to a name that is not UTF-8** (a test plugin that makes `os.mkdir` answer so, as macOS's APFS does), the new file: `31 passed, 5 skipped`. The 5 are the own-directory rows, skipped by the fixture; without it they fail (mutant M9b). - **`tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`:** `146 passed in 31.39s`. The new file copied onto `6ba8c5d2` fails 33 of its 36 tests: `33 failed, 3 passed in 27.36s`. The decisive lines: the probe-refusal rows end `Failed: the caller ended without an error` (the base never asks the probe); the own-directory rows end `Failed: the caller ended SKIPPED: no directory here is named b'caf\xe9': PermissionError(13, 'Permission denied')` (stdout: `this filesystem holds no name that is not UTF-8: [Errno 13] Permission denied`; `directory_named`: `this disk cannot hold the name b'caf\xe9': [Errno 13] Permission denied`); the C-locale rows fail on `UnicodeEncodeError: 'ascii' codec can't encode character ...` and on the `make_file` assert. The 3 that pass on the base are the two disk-error rows (the base's `make_file` test is strict, so they guard against the first version of this change, which skipped: on `41849725` they and the ASCII row fail, `3 failed, 33 passed`) and the fixture row, which holds a fixture the new file defines. The seven existing files (the five, `test_non_utf8_git_reads.py` and `test_non_utf8_name_helpers.py`) pass on the base too (`304 passed, 2 skipped`). Each hunk reverted alone, in a throwaway clone at `6b5e9074`, new file only (36 tests). Every one is caught, by the test it is meant for, except where it says otherwise. | mutant | result | caught by | |---|---|---| | M1 / M2 / M3 / M4: the helper call removed from the stdout, git-reads, template-values and plan-escapes directory test | `2 failed, 34 passed` each | the EILSEQ row and the EACCES-on-the-probe row of that caller | | M1b / M2b / M3b / M4b: the same four with their own `mkdir` guarded by `except OSError: pytest.skip` again | `1 failed, 35 passed` each | the own-directory row of that caller | | M5: the file twin's helper call removed | `2 failed, 34 passed` | the EILSEQ and EACCES rows of `adopt-plan-escapes-file` | | M5b: the file twin's old broad write-and-unlink probe put back | `2 failed, 34 passed` | the same two | | M6a: the helper call removed from `skip_where_this_system_cannot_name` | `3 failed, 33 passed` | EILSEQ, EACCES and the Windows byte row | | M6b: `directory_named`'s own `os.mkdir` guarded again | `1 failed, 35 passed` | the own-directory row of `clone-url-bytes` | | M6c: the old blanket `os.name == "nt"` skip back | `1 failed, 35 passed` | `test_where_windows_holds_a_bidi_override_the_name_check_does_not_skip` | | M6d: the control-character test made `False` | `2 failed, 34 passed` | both Windows control-character rows | | M6e: `directory_named` asks after it makes the directory | `3 failed, 33 passed` | the Windows byte row and both control-character rows (new in the fix round: they call `directory_named` itself) | | M7: the `skip_where_the_environment_cannot_carry(UTF8)` line removed from the `make_file` test | `1 failed, 35 passed` | `test_make_files_utf8_name_test_skips_where_the_encoding_is_ascii` | | M7b: the broad `"filesystem" in reason` skip back in the `make_file` test, the encoding question kept | `2 failed, 34 passed` | `test_a_write_the_disk_refuses_still_fails_the_make_file_test[EACCES]` and `[ENOSPC]` (new) | | M8a: `skip_where_the_environment_cannot_carry` returns instead of skipping | `6 failed, 30 passed` | the `make_file` ASCII row, the three "cannot carry" rows and the two-test rows | | M8b / M8c: its call removed from the text test, or from the every-other-name test | `1 failed, 35 passed` each | the matching row of the two-test group | | M8d: its reason no longer says "file name encoding" | `6 failed, 30 passed` | the same six, on the new `"file name encoding" in reason` asserts | | M9a: the probe fixture `a_disk_that_takes_the_probe` asks nothing | `1 failed, 35 passed` | `test_the_fixture_that_guards_those_rows_asks_the_real_disk` (new) | | M9b: the own-directory rows use the system-only fixture again | `36 passed` on this disk; `5 failed, 31 passed` under a plugin that makes `os.mkdir` answer EILSEQ to a name that is not UTF-8 | **no committed test**: Linux's disk never answers EILSEQ. The probe run is the evidence, the five own-directory rows | | M10: `fail_where_the_caller_ends` never fails, with the stdout helper call also removed | `2 failed, 34 passed` | the same two rows as M1 (`DID NOT RAISE PermissionError` for the EACCES row). The helper is a refactor of what each row did inline; it has no behaviour of its own that Linux can change | `git merge-tree --write-tree HEAD <ref>` at `6b5e9074`, against origin/main (`bf1f099b`), against #723 (`6ba8c5d2`, my parent), against `fix/693-local-remote-dir` (`831d76c6`, the one open branch that edits `tests/test_clone_url_bytes.py`) and against every other wave-7 branch (local refs): all clean except two, which are inherited. At the first push (`41849725`) every `fix/*` and `l3-*` head on origin (24 + 22) was checked too, with the same result. - **`fix/672-bump-leg-branch-and-push-line` (#746, `a5d473ee`) conflicts in `tests/test_non_utf8_git_reads.py`, one region (lines 316-324 of the merged file). It is inherited, not mine:** `merge-tree 6ba8c5d a5d473e` (#723 alone against #746) conflicts in the same file and the same region, and my hunks are at lines 123-127, 497-513, 647 and 668. Whichever of #723 and #746 lands second needs the one conflict resolved by hand, and this branch follows #723. - **`fix/758-git-command-scope` (local `ceca042f`) conflicts in the same file, in the import block (lines 54-62 of the merged file).** Also inherited: `merge-tree 6ba8c5d ceca042f` conflicts in the same lines. - `fix/663-...` (#723), `fix/645-...` (#713) and `fix/662-...` (#748) touch `tests/conftest.py`, which this branch does not edit. `l3-785-non-ascii-paths` (`a1e2c9a8`) touches `tests/test_stdout_encoding.py` and merges clean. - **`make_file`'s own skip** (`tests/test_non_utf8_git_reads.py:100`) turns any `OSError` or `UnicodeError` of the write into a skip. It is the same defect as line 1, in the helper that #723 added, and the issue names it only as the model for the file twin and for line 2. See the candidates. - **A real Windows or macOS run.** Not possible here. - **Two more tests make their own "a name that is not UTF-8, or skip" on any `OSError`:** `tests/test_cli_reference.py:632` (`test_a_checkout_whose_path_is_not_utf8_is_a_checkout`, `parent.mkdir()`, then `pytest.skip`) and `tests/test_doctor_validator_verdicts.py:317` (`here.mkdir(parents=True)`, then `pytest.skip`), at `6ba8c5d2`. Same fix as line 1: `skip_where_a_name_cannot_be_not_utf_8(tmp_path)`, then an unguarded `mkdir`. Neither is in #757's lines. - **`make_file` (`tests/test_non_utf8_git_reads.py:100`) skips on any `OSError` of `path.write_bytes`,** where the helper it calls first raises all but EILSEQ. Only the encoding error (a `UnicodeError` under an ASCII file name encoding) and EILSEQ are the system's answer; a full disk should fail the test. - **`tests/test_adopt_template_values.py:314`** (`test_execute_refuses_a_local_source_directory_holding_a_bidi_control`) makes a directory `U+202E` plus `caf` and skips on any `OSError` ("no directory here is named ..."). The name is text, not a byte that is not UTF-8, so the helper is not its question, but the broad skip hides a full disk the same way. Review: lane 1's re-verification of 6b5e907 is PR-READY (issuecomment-6233426675); 76ae641 restacks it onto main after #723 with the same changed lines. `a1498632` merges main `e62b5e17` after #818: its one conflict, `tests/test_adopt_git_reads.py`'s `from conftest import` line, takes both sides' names, and its -U0 changed lines against main equal the reviewed delta but for that line. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 a149863 passed tests, tests-git-bash and SonarCloud; the head 22238b4 is a clean update-branch merge of main d6475f9, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 9f94a18 over main d6475f9: the adopt_git_reads, adopt_plan_escapes, adopt_template_values, clone_url_bytes, non_utf8_git_reads, non_utf8_name_callers, stdout_encoding, repo_hygiene, agents_md_codes, cli_reference sets, 486 passed, 2 skipped. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green". Closes #757 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #663 (comment)
Test only. No tool and no template changes.
templates/assembly-root/scripts/validate-manifest.pyis a pinned copy and is not edited.No Windows run was made, and no macOS run. CI is Linux only since #597. What Windows and macOS answer is emulated:
os.fsencodeis replaced for one test by Windows' (text.encode("utf-8", "surrogatepass"), three bytes for U+DCFF), and macOS's EILSEQ by amkdirthat answers it.Fix round (head
6ba8c5d2)The review of
0b1d1ddbfound no MUST and one SHOULD and two NITs. All three are taken, in one commit,6ba8c5d2, in three test files:tests/conftest.py,tests/test_non_utf8_name_helpers.pyandtests/test_non_utf8_git_reads.py. No tool file changed.skip_messagehad no test of its own.skip_messageends the test SKIPPED, and no run fails on a skip. The review's mutant SK1 (a skip escapes) gave10 passed, 3 skippedon the helpers file, so seven emulation tests skipped silently.tests/test_non_utf8_name_helpers.pynow holds it with three tests: it returns the reason a call skips with (skip_message(lambda: pytest.skip("x")) == "x"), it returns None where the call does not skip, and it lets any other exception go on (ZeroDivisionError, raised by a small function and not by1 / 0, so no analyser reads a literal division by zero).pytest.fail. A bare assert would not do: the escaped skip would end that test SKIPPED too, and SK1 would still pass.1 failed, 14 passed, 3 skipped(before:10 passed, 3 skipped), and the one failure istest_skip_message_returns_the_reason_a_call_skips_with:Failed: the skip escaped skip_message, which would end the test SKIPPED, and no run fails on that.windows_fsencodetookstronly. Realos.fsencodepasses bytes through.windows_fsencodeintests/conftest.pynow does (if isinstance(text, bytes): return text). Two tests hold it: a surrogate givesb"\xed\xb3\xbf"and text gives UTF-8, and bytes come back as they went in, compared withos.fsencodeitself. Without the pass-through:AttributeError: 'bytes' object has no attribute 'encode'."\\x" not in shownassertion on a Latin-1 file name encoding.make_filewritescafé.ymlas the single byte 0xE9 and the tool, rightly, printscaf\xe9.yml, so the assertion would fail on a tool that is right.assert_shown_without_hex_escapes(shown, output)intests/test_non_utf8_git_reads.py. It skips whereos.fsencode(UTF8) != UTF8.encode("utf-8")and then asserts..ymlending, no traceback, nothing moved). The assertion moved from the middle to the end, so a skip loses none of the others.os.fsencodeit skips, and under Windows' (emulated) it does not skip, still fails a line that holds\xc3\xa9, and passescafé.yml.café.ymlat all (ASCII),make_fileskipped already.Fix-round tests (head
6ba8c5d2,/usr/bin/python3, gh stubbed, targeted files only)0b1d1ddb)tests/test_non_utf8_name_helpers.pytests/test_non_utf8_git_reads.pytests/test_bootstrap_branch_bytes.pytests/test_repo_hygiene.pyThat is 208 passed in two runs (156 in 14.01s and 52 in 19.40s), and no
-rsline printed a skip.tests/test_adopt_pins.pyandtests/test_pin_source_unreadable.pywere not re-run in this round (227 passed on0b1d1ddb): the round changes nothing they import, and neither file useswindows_fsencodeorskip_message.Fix-round mutants
Each in a throwaway clone, one change at a time, with the new test files in place.
skip_message(thetryremoved)1 failed, 14 passed, 3 skipped:test_skip_message_returns_the_reason_a_call_skips_withskip_messageswallows every other exception4 failed, 14 passed:DID NOT RAISE ZeroDivisionError, and the three OSError rowsskip_messagereturns None where a call skips4 failed, 14 passedskip_messagereturns""where a call does not skip8 failed, 10 passedwindows_fsencodewithout the bytes pass-through1 failed, 17 passed:AttributeError: 'bytes' object has no attribute 'encode'windows_fsencodewithoutsurrogatepass3 failed, 15 passedassert_shown_without_hex_escapes1 failed, 1 passed(-k hex_escape): the Latin-1 emulation,'\\x' is contained here: M .github/workflows/caf\xe9.yml1 failed, 1 passed:DID NOT RAISE AssertionError2 failedscripts/bump-leg.py:1126decodes with"ascii"2 failed, 4 passed(-k "utf8_under_quote_path_false or hex_escape"):[bump]and[dry-run],'\\x' is contained here: M .github/workflows/caf\xc3\xa9.ymlNot probed: the real test's call to the helper. Dropping it drops the check, and a Latin-1 host is not here to show it. R1 failing with the call in place is the evidence that the call asserts.
Also measured, on a host emulated as ASCII (
PYTHONUTF8=0 LC_ALL=C PYTHONCOERCECLOCALE=0,sys.getfilesystemencoding()isascii): the helpers file gives18 passed, and the two utf8 rows skip inmake_file('ascii' codec can't encode character '\xe9'). No Latin-1 locale is installed, so the Latin-1 host itself is emulated byos.fsencodeonly, not run.Merge check
git merge-tree --write-tree HEAD origin/main(2fac9790): clean, rc 0, tree3e71db8d. The other branches below were checked at0b1d1ddb; this round adds seven tests, one helper function and a small change towindows_fsencode, and was not re-run against them.What changed
Base: origin/main
8062810d. Branch head:6ba8c5d2(three commits:d6d1df23and0b1d1ddb, then the fix round).tests/conftest.pyholdsskip_where_a_name_cannot_be_not_utf_8(directory).tests/test_adopt_pins.pyandtests/test_pin_source_unreadable.pyeach had a copy of the body; both copies are gone and both call the shared one. It asks two questions, in this order:skip_where_fsencode_gives_a_surrogate_no_byte(text): skips wheretextholds a byte-carrying surrogate (U+DC80 to U+DCFF) andlen(os.fsencode("\udcff")) != 1. That is Windows.probe\udcffunder the directory and removes it. EILSEQ (macOS) skips; any otherOSErroris raised.make_file(R423 N1).tests/test_non_utf8_git_reads.pycalls the first question before it writes, for a name that holds the byte. A name that is text (café.yml) is still written. Thetry/exceptstays, for APFS. The module docstring andmake_file's docstring now say who skips and why.--branch a\udcffbtest (R455 F1).tests/test_bootstrap_branch_bytes.pycalls the shared helper first, in place of its own probe of a file.test_a_branch_the_system_carries_has_no_faultasked the sameos.fsencodequestion inline; it now asks the same function, so the rule is written once. The file's own emulated testtest_where_a_surrogate_is_three_bytes_and_no_byte_it_is_refusedkeeps the Windows half, unchanged.test_a_name_that_is_utf8_under_quote_path_false_is_still_refusedtakes the one stderr line that startsM .github/workflows/cafand asserts it holds no literal\x, throughassert_shown_without_hex_escapes, which skips where this system's file name encoding is not UTF-8 (fix round, NIT-1). A locale decode of UTF-8éis mojibake at worst and never a\x, so the ASCII-ends caution stays._leg_branch_findings([{"role": "spec", "path": "contracts", "branch": "a\x00b"}])and holds that the line startsFINDING manifest-leg-branch: leg 'spec': branch is 'a\x00b', which holds U+0000, a NULand does not holdcontracts. It runs forspecandcode, onvalidate-manifest.pyloaded byload_without_bytecode. The file's last test now takes the module from a module fixture,validator_module, which the new test shares.tests/test_non_utf8_name_helpers.py(18 tests: 13 in the first round, 5 in the fix round) holds the helpers to this by emulation:os.fsencodegives three bytes;skip_messageandwindows_fsencodeare held too.--branchtest and the no-fault test are each called, as they are, under Windows'os.fsencode, and must skip.make_fileis called the same way: it skips and writes nothing, and a UTF-8 name is still written.Shared helpers in
tests/conftest.py, all in one block aftera_hook_that_unstages, away fromfix/645-fixture-copy-maintenance-race's hunks at :85 and :137:NOT_UTF_8_PROBE,skip_where_fsencode_gives_a_surrogate_no_byte,skip_where_a_name_cannot_be_not_utf_8,windows_fsencode(which passes bytes through, asos.fsencodedoes) andskip_message.skip_message(call, *args)returns the reason a call skips with, or None. A barepytest.raises(pytest.skip.Exception)is not enough for a test of a skip helper: a helper that skips where it should not ends the test SKIPPED, and no run fails on a skip.Decisions
make_filehas no directory to probe: its write is the probe. So the system's question is its own function, and the directory helper calls it first. A change to what Windows answers is made once.test_where_fsencode_makes_three_bytes_the_disk_is_not_askedpasses a directory that does not exist: anmkdirunder it would raise FileNotFoundError, and the skip is the proof the disk was not asked.windows_fsencodeis in conftest.test_where_a_surrogate_is_three_bytes_and_no_byte_it_is_refusedhas the same lambda inline. I left it, to keep the hunks small.pytest.failin anexcept pytest.skip.Exceptionis the only way the test forskip_messagecan fail on SK1.assert_shown_without_hex_escapesit is called with an emulatedos.fsencode, and fails a test when its skip or its assertion is removed.Tests (first round, head
0b1d1ddb,/usr/bin/python3, gh stubbed, targeted files only)tests/test_non_utf8_git_reads.pytests/test_bootstrap_branch_bytes.pytests/test_non_utf8_name_helpers.pytests/test_adopt_pins.py+tests/test_pin_source_unreadable.pytests/test_repo_hygiene.pyNo test skipped on Linux (
-rsprinted none). No test file importsskip_where_*besides those four and the new file. The fix round's counts are above.Mutation probes (first round)
Each in a throwaway clone at the head, one change at a time. "Base file" is the file as it was on
8062810d, run under the mutant.scripts/bump-leg.py:1126decodes with"ascii"test_a_name_that_is_utf8_under_quote_path_false_is_still_refused[bump]and[dry-run],'\\x' is contained here: M .github/workflows/caf\xc3\xa9.ymlleg.get('path')in place ofleg.get('role')test_the_leg_finding_names_the_leg_by_its_role_and_not_its_path[spec]and[code],leg 'contracts': branch is ...make_filetest_make_file_skips_where_fsencode_makes_no_byte_and_writes_nothing,make_file wrote the name instead of skipping--branchtesttest_the_branch_test_skips_where_fsencode_makes_no_byte_of_the_surrogatetest_the_no_fault_test_skips_where_fsencode_makes_no_byte_of_the_branch..._the_disk_is_not_asked(FileNotFoundError)OSErrortest_any_other_error_from_the_disk_is_the_tests_own(DID NOT RAISE OSError)test_a_disk_that_takes_the_name_is_not_skipped_and_the_probe_is_removedtest_where_fsencode_makes_three_bytes_a_name_that_is_text_is_not_skipped== 1)Reproduction
All rows are local; no
gh, no network. The issue's rows are about what a test does on Windows or macOS, so they are shown by emulation on Linux, and by the mutants above (a mutant that drops a guard is what base did):git grep -n 'skip_where_a_name_cannot_be_not_utf_8\|skip_where_no_file_name_is_not_utf_8'on base shows two bodies; on the head it shows one intests/conftest.pyand four call sites.make_filewritescaf\udce9.ymland the branch test runs on. On the head both skip under Windows'os.fsencode.Merge checks (first round, head
0b1d1ddb)git merge-tree --write-tree HEAD <ref>, with the refs fetched read-only (and deleted after):de926142: clean. Main had moved since8062810dby bump-leg.py and setup-project.py: thegit switch -cline says under it what to do where an earlier act left that branch, soalready existsis no dead end (#607's sentence, in the two tools it left) #657 (scripts/bump-leg.py), the Git Bash change and a doctor test; none touches these files. (The fix round's check against2fac9790is above.)fix/645-fixture-copy-maintenance-race: clean (its conftest hunks are at :85 and :141).fix/651-adopt-annotated-tag-pin(lane 1; editstests/test_adopt_pins.py): clean.fix/664-surviving-mutants(lane 4; touches none of these files): clean.fix/655-adopt-doctor-plan-overrides(editsvalidate-manifest.py, which I do not): clean.fix/388,389,417,425,610,646,648,650,653,658,659,696,l3-668,669,677,678,l3-rehearsal-v1.1.0): clean.docs/release-notes-v1.0.0andl3-563-docsconflict (docs/release-notes/v1.0.0.md,docs/handbook.html); they conflict with origin/main alone, so it is not this change.fix/674-update-shape-applywas not on origin yet, and nol1-*branch existed on origin.Left out
tests/test_stdout_encoding.py:209,tests/test_adopt_git_reads.py:431,tests/test_adopt_template_values.py:296andtests/test_clone_url_bytes.py:143(directory_named). They build the name from bytes, which raises on Windows, so they skip there already. They are not in tests: the non-UTF-8 name and branch tests: one skip helper for a filesystem that refusesprobe\udcff, Windows guards onmake_fileand the--branch a\udcffbcase, and the UTF-8 promise and the leg finding's role pinned #663's lines.test_where_a_surrogate_is_three_bytes_and_no_byte_it_is_refusedkeeps its inline lambda.Checklist candidates (for the coordinator to place; none filed)
tests/conftest.pynext toskip_where_a_name_cannot_be_not_utf_8would give a fifth name for the same question one place to change. v1.1.1, optional.test_make_file_writes_a_name_that_is_utf8_where_fsencode_makes_no_bytefails on an ASCII file name encoding. Found in the fix round, and it was so at0b1d1ddb: on a host withPYTHONUTF8=0 LC_ALL=C PYTHONCOERCECLOCALE=0(sys.getfilesystemencoding()isascii),make_filecannot writecafé.yml, skips withthis filesystem cannot hold a file name that is not UTF-8, and the test'sassert reason is Nonefails (1 failed, 2 passedfor-k make_file). Nothing in CI has that encoding (Linux, macOS and Windows are UTF-8 by default), and a host needs three opt-outs to get it. The direction is the neighbouring control test's own two lines:if reason is not None and "filesystem" in reason: pytest.skip(reason). v1.1.1, optional.Closes #663
🤖 Generated with Claude Code
Summary by Sourcery
Centralize and strengthen portability tests for non-UTF-8 filenames and branch names across supported filesystem behaviors.
Enhancements:
Tests: