Repository navigation
validate-pins.py and repo_shape.py: gh's stderr that is not UTF-8 is a skip and no traceback, JSON that is not a tree listing is gh-response-unreadable, every gh detail spells the repository, a leg's git directory this user cannot search is refused by name, and what stands where contracts should be is named (#968) - #985
Conversation
…a skip and no traceback, JSON that is not a tree listing is `gh-response-unreadable`, every gh detail spells the repository, a leg's git directory this user cannot search is refused by name, and what stands where `contracts` should be is named (#968) Item 1. `tree_digest_from_gh` reads gh's two streams as bytes and decodes each with `_child_text`: stderr with `errors="replace"`, as `git_out` reads git's, and stdout strictly, as #217 asks of a listing that becomes a digest. A CR is still a line break, as text mode made it. One byte of gh's stderr that is not UTF-8 ended the run in a `UnicodeDecodeError` traceback, exit 1, where a skip belongs; it is U+FFFD in the skip now. A byte that is not UTF-8 on stdout, and JSON that is not the listing's object (a list, `null`, a row that is no mapping or lacks a column, a path holding a lone surrogate), is `gh-response-unreadable`, where each was a traceback. Item 2. `_check_pin_digest` asks `_refuse_an_unreadable_git_dir` after the uninitialized check: the `stat` of `HEAD` in the leg's git directory, the `.git` itself where it is a directory, or the one its gitfile's `gitdir: ` line names (`_leg_git_dir`, line ends stripped as git strips them). A refused `stat` is `pin-submodule-unreadable` through `_unreadable_file`, with the leg checkout's remedy and `_is_a`'s errno set. `spec/.git` at mode 000 ended `pin-commit-unresolvable` with an empty reason, and `.git/modules/spec` at mode 000 in `fatal: not a git repository`, both under THE LOCKSTEP RULE. A git directory with no `HEAD`, or a gitfile that names none, is left to git as before. Item 3. `_no_file` asks `_not_a_directory` of the directory the file is in where nothing stands at the file's name: `contracts` replaced by a file, a FIFO, a socket or a symlink that leads to no directory is named (`... is under contracts, which is a file, not a directory`), with `NOT_A_DIRECTORY`'s remedy, where every pin read "does not exist" under the lockstep remedy. `_what_stands` is #919's `_not_a_file` reading, shared by both. Item 4. `gh-response-unreadable` and `gh-tree-truncated` name the repository with `shown_path`, as #919 named it in `gh-unreadable`. Item 5. `tests/test_validate_pins_spelled_and_named.py` gains a socket in each file's place (rv919's r2) and a `Refusal` that defaults to a remedy, as one older than #370 does, at the three sites that give none (r4), with the tests of items 1 to 4. Its `subprocess.run` stand-in hands bytes, and `tests/test_git_output_decoding.py` drops the `EXEMPT` row for this call, which asks for no text now. Both files are pinned copies; `validate-pins.py` imports nothing new from `repo_shape`, and every state #968 does not name prints byte for byte what it printed, beside the current and the pre-#370 `repo_shape.py`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-18T20:13Z) @codex review |
Reviewer's GuideThe pull request hardens GitHub tree parsing and pin validation against undecodable or malformed child-process output, inaccessible Git metadata, and filesystem objects replacing expected directories, while adding comprehensive cross-platform, compatibility, and mutation-tested diagnostics. Sequence diagram for robust GitHub tree response handlingsequenceDiagram
participant Validator
participant GH as gh
participant Parser as tree_digest_from_gh
Validator->>Parser: tree_digest_from_gh(repository, commit)
Parser->>GH: gh api tree endpoint
GH-->>Parser: stdout bytes and stderr bytes
alt nonzero exit
Parser->>Parser: _child_text(stderr, replace)
Parser-->>Validator: Refusal gh-unreadable with shown_path(repository)
else successful exit
Parser->>Parser: _child_text(stdout, strict)
alt invalid UTF-8 or malformed tree listing
Parser-->>Validator: Refusal gh-response-unreadable
else truncated listing
Parser-->>Validator: Refusal gh-tree-truncated with shown_path(repository)
else valid tree listing
Parser-->>Validator: tree digest
end
end
Flow diagram for filesystem-aware pin validationflowchart TD
Start[_check_pin_digest] --> Init[Check submodule initialization]
Init -->|uninitialized| RefuseInit[Refuse pin-submodule-uninitialized]
Init -->|initialized| GitDir[_refuse_an_unreadable_git_dir]
GitDir --> Resolve[_leg_git_dir]
Resolve --> Head[Check git directory HEAD with _is_there]
Head -->|stat refused| RefuseGit[Refuse pin-submodule-unreadable with checkout remedy]
Head -->|readable or absent| Git[Resolve pinned commit with git]
Git --> Digest[Validate pin digest]
Missing[_no_file] --> File[_not_a_file]
File -->|unexpected object| RefuseFile[Name object and use file remedy]
File -->|nothing| Holder[_not_a_directory on parent]
Holder -->|parent is not directory| RefuseDir[Name holder and use directory remedy]
Holder -->|directory or absent| MissingPin[Keep missing-file behavior]
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. |
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c8b6d0fa67
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
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".
| records = sorted( | ||
| f"{row['mode']} {row['type']} {row['sha']}\t{row['path']}" | ||
| .encode() | ||
| for row in payload.get("tree") or [] if row.get("type") != "tree") |
There was a problem hiding this comment.
Reject a missing or falsy tree payload
When gh api exits successfully but returns {}, {"tree": null}, {"tree": 0}, {"tree": false}, or {"tree": ""}, payload.get("tree") or [] treats the malformed response as a valid empty listing and returns SHA-256 of the empty byte string. This can falsely report pin drift, or even accept an empty-tree digest, instead of producing the intended gh-response-unreadable skip; validate that tree is present and is a list before computing records.
Useful? React with 👍 / 👎.
|
Independent review of #985 (#968) at I found no MUST, SHOULD or NIT. Each of #968's items is fixed as the issue asks. A fresh six-variant probe finds no change outside what the issue names. Every count and merge result in the body that I measured again holds. FindingsNone at MUST, SHOULD or NIT. The two things below that a later change could pick up are checklist candidates, not defects of this PR. Checked and trueEach #968 item. The probe is mine (
Probe (76 cases, 456 records). I ran six pairs of the two files:
Results (
The pinned-copy rule.
Mutants.
Base-fail. Both changed test files run against Runs at
Collecting the writer's 16-file list gives 1167 tests, and the changed test file holds 106, both as the body says. Merges.
Sonar-style checks.
Could not verify
Checklist candidates
|
|
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 2 days and 8 hours by commenting |
…user cannot read is refused by name, `member-unreadable`, exit 2, so Python 3.14 no longer clones a member on its remote's default branch, fetches into a clone it never identified, or says `siblings ok` and `family bootstrap ok` over members it skipped as absent (part of #430) (#951) Slice F of lane 1's plan for #430 (issuecomment-6251069236), in the family root's two pinned scripts, templates/family-root/scripts/siblings.py and bootstrap.py. Each guard asked `Path.is_file()` or `Path.exists()` of a member's path. Python 3.12 and 3.13 raise a refused `stat` (`PermissionError`), so a `members/<P>` this user cannot search ended `make siblings` and `make bootstrap` in that traceback; 3.14 answers False to any `OSError`, so the path read as absent. Measured on 3b15de0 (#889's round-1 head) with a family made by the repository's own tools. Under a real mode on Python 3.12.3 every site below ended in a `PermissionError` traceback, exit 1. Under a 3.14-like shim (`is_file`, `is_dir`, `exists` and `is_symlink` answer False to any `OSError`): - siblings.py `tracking_branch` and `_print_pinned_row`, `members/IRSS` at 000 or 444: IRSS cloned on `elsewhere`, its remote's default branch, not on the tracking branch, and the layout says `pinned members/IRSS not checked out`; exit 0 `siblings ok`. - `project_id`, a clone's `project.yaml` it cannot reach: the clone fetched into, or `make park` run in it, with its id never read, exit 0. - `run_member_bootstrap`: "no scripts/bootstrap.py; cloned and left as it is", its legs never placed, exit 0. `dispatch`'s `Makefile`: "has no Makefile". - bootstrap.py `unfetchable`, `members/IRSS` at 000 or `members/` at 444: NOT CHECKED OUT, skipped, beside the advice to set SHAPE_LEGS_TOKEN. `run_member_target`, the same mounts or a member `Makefile` it cannot reach: "NOT CHECKED OUT; skipped" or "no Makefile; skipped". Both exit 0 `family bootstrap ok`. - `members_of`, a `family.yaml` it cannot reach: `family-manifest-missing`, "does not exist". Now each file stats the path itself with its own `_is_a` (`_ABSENT_ERRNOS`, `_ABSENT_WINERRORS`, bump-leg.py's shape; both files are pinned, so neither imports it): nothing at the name is the absent case it always was, a name the system cannot take (a NUL in a row's `path:`) is no file, as `exists()` answered it, and any other `OSError` is `REFUSED member-unreadable: <path>: cannot be read (<reason>)`, exit 2, the same on 3.12 and 3.14. A permission gets validate-pins.py's `chmod u+r` / `chmod u+rx` advice (bootstrap.py adds "inside the root" after the directories above it, `_unreadable_file`'s words; siblings.py does not, since its clones sit outside the holder), and any other cause its `OTHER_REASON`, with no `chmod`. `family.yaml` is `family-manifest-unreadable`, the code a `family.yaml` that is no mapping already has. - siblings.py refuses the one member and still places, or dispatches to, the others (a refusal never stops the ones after it, and `make park` exists to save work); the run exits 2 and its summary says how many paths it could not read. A refused `scripts/bootstrap.py` says the clone was made and to run `python3 scripts/bootstrap.py` in it, since a second `make siblings` only fetches. - bootstrap.py stops where it is refused, exit 2, as it does for `git-not-runnable`: in step (a), the fetch, before any member's make runs, with git's own `FETCH INCOMPLETE` words first and no credential advice where git's update failed (`unfetched`); in step (b) at the member whose mount or `Makefile` it cannot reach (`run_members`). `main` catches the refusal from either once, stdout flushed first. Rounds and review. The writer is lane 4's W430F; the reviewer is lane 5's rv951. - Round 0, 528adbf, stacked on #889's 29083a5. Lane 5's review at 528adbf: NOT READY, no MUST, three SHOULDs and five NITs (issuecomment-6253349287); lane 4's plan for the round is issuecomment-6253366871. - Round 1, 3c5f2ea, a fast-forward on 528adbf (issuecomment-6254509055). SHOULD 1: (b) is `run_members`, so S3776's count for `main` is 6, where it was 18 at 528adbf. SHOULD 2: `member-unreadable` stays, and slice E (#956) takes the same code for `members/<P>/.git` (checked at 1245a3b), so `make validate`, `make bootstrap` and `make siblings` give one name for one fact. SHOULD 3: a NUL in a name is no file. NIT 4: the placement summary's words are tested. NIT 5: the reason `parent_folder_warning` is left alone is corrected. NIT 6: git's own words come before step (a)'s refusal. NIT 7: the errno tests are three, with true names. NIT 8: bootstrap.py says "inside the root". - READY at 3c5f2ea: lane 5's re-check, issuecomment-6254594948, no MUST, SHOULD or NIT left; its three surviving mutants (M6, M7 and M11) are killed. - Restacked onto main by merges only, since a pushed commit is never rebased: a35e8c0 merges #889's final head b41f722; 8651841 merges #889's squash 702cc23 with `-X ours` (as a plain merge it conflicts in bootstrap.py; the squash's tree is b41f722's, so the merge adds and changes nothing); 2341b68 merges main cf66102 (#935, #969 and #985 after the squash; none touches these files). Lane 4's reply is issuecomment-6266695040. - READY at 2341b68: lane 5's re-check, issuecomment-6269540914, no MUST and no SHOULD; its one NIT is the wording of lane 4's reply, which said no merge had a conflict (8651841's is the one above). #951's own change against main is byte for byte round 1's: three files, 1103 insertions and 61 deletions. tests/test_family_siblings_bootstrap_unreadable.py (new, 57 tests). Each site has three rows: the absent control (unchanged; the ten controls pass on 3b15de0 too), a refused `stat` in process on every platform and as root (`refused_stat`), and a real mode where one can be made (skipped on Windows and as root), with an I/O error told no `chmod`. Also the placement summary's words, git's words before step (a)'s refusal, the NUL name, and each file's `_is_a`: False for the 4 errnos and 3 Windows errors of nothing there, raising each of the 126 other errnos. The helpers are imported from tests/test_bump_leg_unopenable.py and tests/test_doctor_unreadable_pin.py, none copied; conftest.py is not touched. Round 0's file (35 tests) was 25 failed and 10 passed on 3b15de0; all 57 pass. Runs (lane 4's on Python 3.12.3, with a stub `gh` first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR and GIT_SSH_COMMAND=false): - 3c5f2ea, one targeted run: 2538 passed, 10 skipped (git filter-repo is not installed) in 26m05s, over 30 files (the new file, the 26 test files that run or load the family root's siblings.py or bootstrap.py, and the trio: tests/test_repo_hygiene.py, tests/test_cli_reference.py and tests/test_agents_md_codes.py) and the 19 tests of tests/test_shape_doctor.py that load or run them. - 2341b68: the new file, all of tests/test_shape_doctor.py, tests/test_scaffold_leg_path_collisions.py and the trio: 772 passed, 7 skipped in 13m44s. - Lane 5's, on git 2.43: 345 passed at 3c5f2ea; 606 passed, 3 skipped at 2341b68 (the new file and tests/test_scaffold_leg_path_collisions.py, 161; tests/test_shape_doctor.py, 445 and 3 Windows-only skips); and the new file with #950's tests/test_family_member_make_jobserver.py on a merge with main c7a3ff1, 100 passed. 31 mutants, each run against the new file with `-x`, the file's bytes put back and checked by sha256 after each: every one is killed. Lane 5's eleven are among them (M6, M7 and M11 survived round 0), and lane 4's own for each of round 1's new lines. Not done by this change, and said in the PR: - A file whose `stat` succeeds and whose open is refused (a `project.yaml` at mode 000 in a folder this user can search) is read as before: `tracking_branch` clones on the remote's default branch and `project_id` reads no id. It is no absence read, and it predates #430. - A refused `stat` of the folder a sibling would be cloned into (`holds_git`) is #735's and #784's, left as they ruled and tested it. `parent_folder_warning`'s `wanted.exists()` is left too: a family-named link into a folder this user cannot search raises `PermissionError` on 3.12 and says "it is a link to nothing" on 3.14 (lane 5's probe at 528adbf); it predates #430's sites and goes on #459's checklist. - No Python 3.14 interpreter was run: the 3.14 half is `refused_stat` in process and a shim on the command line. Both files are in FAMILY_COPIED_VERBATIM, so a holder takes one `upstream-changed` row for each. Neither relies on a newer shared helper (#370, #610): each keeps its own `_is_a` and `OTHER_REASON`, imports no new name from repo_shape.py (which is untouched), and siblings.py imports no new name from bootstrap.py, so either taken alone runs beside the other's older copy. Part of #430 (slice F; lane 1's plan, issuecomment-6251069236). #430 stays open for its other slices. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #968 (comment)
What #919 left, from rv919's review of #952 at
8a7671b3: the five items of #968, and JSON from gh that is not a tree listing, which ended in a traceback too.2f5cabf5(validate-pins.py and repo_shape.py:--rootand gh's stderr are spelled, what stands in a pin's, the manifest's or a shape copy's place is named, and a leg's.gitgitfile this user cannot open is refused by name (#919) #952). The head isc8b6d0fa.validate-pins.pywith a stubghfirst on PATH, on git 2.43.0. Below,<ESC>is the byte 0x1B and<FF>the byte 0xFF.Item 1: gh's stderr that is not UTF-8, and gh's JSON
Change.
tree_digest_from_gh(scripts/repo_shape.py) reads gh's two streams as bytes and decodes each with a new helper,_child_text. A CR is still a line break, as text mode made it.errors="replace", asgit_outdecodes git's. It is words for a person, andspelledalready makes it one printable line (validate-pins.py and repo_shape.py:--rootand gh's stderr are printed raw, a non-file in a pin's place is called absent, and a leg's.gitgitfile at mode 000 passes #919); a replaced byte (U+FFFD) says where a byte was.run()decodes every git andghmessage with the locale codec, so a push rejection or any other failure that names a non-ASCII path raisesUnicodeDecodeErrorin place ofREFUSEDand the ruleset hint under a non-UTF-8 locale, after both leg remotes exist #217): this JSON becomes a digest. A byte that is not UTF-8 there isgh-response-unreadable.null,treethat is a number, a row that is not a mapping or has nomode, a path holding a lone surrogate) isgh-response-unreadable,<repository> @ <commit>: the answer is not a tree listing (<error>).Probe. A declared
openXdoxpin with no local source.bad<FF>byte. Before: aUnicodeDecodeErrortraceback fromsubprocess's_translate_newlines, exit 1. After, exit 0 andpins ok, with the skip... (gh-unreadable:gh apicould not read testorg/openXdox @ cccc...: bad<U+FFFD>byte). ...{"tree": [], "x": "<FF>"}. Before: the same traceback, exit 1. After: the skip namesgh-response-unreadable: testorg/openXdox @ cccc...: 'utf-8' codec can't decode byte 0xff in position 19: invalid start byte.[]. Before:AttributeError: 'list' object has no attribute 'get', a traceback, exit 1. After: the skip namesgh-response-unreadable: ... the answer is not a tree listing (AttributeError: 'list' object has no attribute 'get').null,{"tree": 5},{"tree": ["x"]}, a row with nomodeand a lone surrogate read the same way, each with its own error.Refusalgives every refusal built without a remediation the assembly root's own (run git submodule update --init --recursive, then python3 scripts/validate-pins.py ... contracts/<leg>-pin.yaml), so in a family holderfamily.py addover an unparsable.gitmodules(git-failed),bumpon afamily.yamlof the wrong kind, and the holder's ownvalidate-family.pyon a shortpin.commiteach tell the operator to run a file no holder carries and to move a leg pin a holder does not have #370repo_shape.py.Item 2: a leg's git directory this user cannot search
Change.
_check_pin_digest(validate-pins.py) calls_refuse_an_unreadable_git_dirafter the uninitialized check. It finds the leg's git directory with_leg_git_dir: the.gititself where it is a directory, or the directory named by a gitfile'sgitdir:line, with line ends stripped as git strips them. Then it stats theHEADin it, as_is_thereasks. A refusedstatispin-submodule-unreadablethrough_unreadable_file, with_is_a's errno set and the leg checkout's remedy that #827 gives a checkout this user cannot search. A git directory with noHEAD, or a gitfile that names none, is left to git, as before.Probe.
.git/modules/specat mode 000 (or r--), gitfile readableREFUSED pin-commit-unresolvable: spec: the pinned commit ... (... exited 128: fatal: not a git repository: <root>/spec/../.git/modules/spec), lockstep remedyREFUSED pin-submodule-unreadable: .git/modules/spec: cannot be read (Permission denied), the checkout remedyspec/.gita directory at mode 000 (or r--)REFUSED pin-commit-unresolvable: ... exited 1: ), lockstep remedyREFUSED pin-submodule-unreadable: spec/.git: cannot be read (Permission denied), the checkout remedyThe same leg with
spec/.gita searchable directory passes before and after, and so does a gitfile ending in CR LF.Item 3: what stands where
contractsshould beChange.
_no_file(validate-pins.py) now takes the root. Where nothing stands at the file's name, it asks_not_a_directoryabout the directory the file is in. Something that is not a directory there is named, with a remedy of its own:_what_standsis #919's_not_a_filereading, now shared by both:lstat, thenstatthrough a symlink, opening nothing.Probe.
contractsmoved aside and replaced. The first pin a run asks is the leg's.REFUSED pin-missing: leg 'spec' declares testorg/Atlas-spec at 'spec' but contracts/spec-pin.yaml does not exist, lockstep remedy.... but contracts/spec-pin.yaml is under contracts, which is a file, not a directory, and the same witha FIFO,a socket,a symlink to nothing,a symlink loopanda symlink to a file, each with the remedy above.contractsabsent, andcontractsa symlink to the real directory, print what they printed.The shape pin and a neutral-product pin, which a run reaches only after the legs, say the same in-process. The manifest's directory is the root, which always is one.
Item 4: every gh detail spells the repository
Change.
gh-response-unreadableandgh-tree-truncatedname the pin's repository withshown_path, as #919 did forgh-unreadable.Probe, in-process, with the repository
testorg/x<ESC>[31mRED:REFUSED gh-tree-truncated: the forge truncated its tree listing for testorg/x<ESC>[31mRED @ cccc..., so ..., with the ESC raw.... for 'testorg/x\x1b[31mRED' @ cccc..., so ....gh-response-unreadableis the same. A printable repository is named byte for byte as before.Item 5: tests that kill r2 and r4
is a socket, not a file). It is bound by its name after achdir, because AF_UNIX takes about 100 bytes and a temporary path need not fit. POSIX only.Refusal, as the gate builds one, replaced by a subclass whose default is a remedy, as a pre-repo_shape.py'sRefusalgives every refusal built without a remediation the assembly root's own (run git submodule update --init --recursive, then python3 scripts/validate-pins.py ... contracts/<leg>-pin.yaml), so in a family holderfamily.py addover an unparsable.gitmodules(git-failed),bumpon afamily.yamlof the wrong kind, and the holder's ownvalidate-family.pyon a shortpin.commiteach tell the operator to run a file no holder carries and to move a leg pin a holder does not have #370repo_shape.py's is. Then the leg pin, the shape pin andproject.yamlare taken away. Each refusal keeps that default, so it was built with no remedy, not with an empty one.Output not named here is unchanged
probe968.pyis rv919'sprobe919.pywith every case run on all four variants and 28 cases added: 89 cases, 356 records. The variants are the base and the headvalidate-pins.py, each beside the base or the headrepo_shape.py, and each beside the pre-#370repo_shape.py(b0826e02). The probe tree is under_lane5-scratch, not$Q.cmp968.py: every case validate-pins.py and repo_shape.py after #919: gh's stderr that is not UTF-8 ends in a traceback, a leg's git directory this user cannot read is called an unresolvable pin with no reason, and two gh details still name the repository raw #968 does not name is byte-identical between base and head (stdout, stderr and exit), beside bothrepo_shape.pyversions. That includes rv919's 61 cases and the new controls:contractsabsent or a symlink to the real directory, a leg's own.gitdirectory that can be read, a gitfile ending in CR LF, a gitfile naming nothing, a garbage or empty gitfile,HEADat mode 000, a truncated or valid listing, and JSON that does not parse.Pinned copies
Pinned copies: every project re-pins
validate-pins.pyandrepo_shape.py.validate-pins.pyimports nothing new fromrepo_shape(#370, #610).Tests
tests/test_validate_pins_spelled_and_named.py, 106 tests (it had 51):ghprinting a byte that is not UTF-8 (POSIX), with the skip line exact; that byte in-process; stdout that is not UTF-8; six kinds of JSON that is not a listing; a valid listing's digest, with CR LF..git/modules/specandspec/.gitat mode 000 and r--, run as a project runs them (they skip as root and on Windows); a refusedstatofHEADin-process, a permission and an I/O error; a missingHEADleft to git;_leg_git_dirfor a relative, CR LF, unterminated, blank-line-after and absolutegitdir:, a directory and a gitfile naming none.contractsreplaced by a file, a FIFO, a socket, a symlink to nothing, a loop and a symlink to a file, whole stderr exact; the shape-pin and neutral-pin sites in-process;contractsabsent, byte for byte.gh-response-unreadable(bad JSON, and JSON that is not a listing) andgh-tree-truncatedfor four unprintable repositories and a printable one.subprocess.runstand-in hands bytes, as the code now reads them.tests/test_git_output_decoding.py: theEXEMPTrow fortree_digest_from_ghis gone, since that call asks for no text now.Fail on base. Both files run against
2f5cabf5's code: 51 failed, 122 passed. The new tests that pass there by design:--rootand gh's stderr are printed raw, a non-file in a pin's place is called absent, and a leg's.gitgitfile at mode 000 passes #919's code already does what they pin, and they exist to kill those mutants;contractsabsent, a valid listing's digest, the printable repository's rows for bad JSON and a truncated listing, and a missingHEADleft to git.Eight of #919's in-process gh rows fail there only because the stand-in now hands bytes, where the base asked for text.
Mutants
Each mutant is applied alone to a scratch tree at the head and run against the two changed test files. All 23 are killed.
test_gh_s_stderr_that_is_not_utf_8_is_a_skip_and_no_tracebackreplacetest_gh_json_that_is_not_utf_8_is_unreadable_and_no_tracebacktest_gh_s_stderr_in_the_skip_line_is_spelled_on_one_lineKeyErrorcaught for JSON that is not a listingtest_every_gh_refusal_names_the_repository_spelled[not-a-listing-newline]UnicodeEncodeErrornot caughttest_json_that_is_not_a_tree_listing_is_unreadable[a-lone-surrogate]test_gh_s_stderr_in_the_skip_line_is_spelled_on_one_linetest_the_git_directory_a_leg_gitfile_names_unsearchable_is_refused[000]spec/../.git/modules/spec)test_the_git_directory_a_leg_gitfile_names_unsearchable_is_refused[000]gitdir:line's line end kepttest_the_git_directory_a_leg_gitfile_names_unsearchable_is_refused[000]HEADtest_the_git_directory_a_leg_gitfile_names_unsearchable_is_refused[000]test_the_git_directory_a_leg_gitfile_names_unsearchable_is_refused[000].gitdirectory not askedtest_a_leg_git_directory_this_user_cannot_search_is_refused[000]test_what_stands_where_contracts_should_be_is_named[file]test_a_file_that_is_not_there_keeps_its_words_byte_for_byte[leg-pin]test_what_stands_where_contracts_should_be_is_named[file]test_what_stands_where_contracts_should_be_is_named[file]which is not a directory)test_what_stands_where_contracts_should_be_is_named[file]test_what_stands_in_a_files_place_is_named_with_a_remedy_that_fits[leg-pin-symlink-to-a-directory]gh-response-unreadablenames the repository rawtest_every_gh_refusal_names_the_repository_spelled[not-json-newline]gh-tree-truncatednames it rawtest_every_gh_refusal_names_the_repository_spelled[truncated-newline]test_every_gh_refusal_names_the_repository_spelled[not-a-listing-newline]test_what_stands_in_a_files_place_is_named_with_a_remedy_that_fits[leg-pin-socket]test_a_refusal_with_no_remedy_of_its_own_is_built_with_none[leg-pin]Runs
Through
run9.sh(stubghfirst on PATH, bogus token), at the head's tree:test_validate_pins*files,test_pinned_copy_text.py, the trio (test_repo_hygiene.py,test_agents_md_codes.py,test_cli_reference.py),test_pin_validator.py,test_neutral_pin_validator.py,test_doctor_validator_rows.py,test_doctor_validator_verdicts.pyandtest_pin_source_env_collisions.py.Merge checks
git merge-tree --write-treeis clean against origin/mainebbd9fa4, fetched before the push, and against lane 3'sl3-917-yaml-readere63ca75c, which editsrepo_shape.py's YAML reader. The commits on main since2f5cabf5touch none of this change's four files.Closes #968
🤖 Generated with Claude Code
Summary by Sourcery
Harden pin validation and GitHub tree handling so malformed responses and inaccessible repository structures produce clear, actionable outcomes instead of tracebacks.
New Features:
Bug Fixes:
Enhancements:
Tests: