test: cover the request plumbing and decoding branches of the Python client - #8
Merged
Merged
Conversation
…client
Characterisation tests only -- no production code changes. These lock in
behaviour that every public method inherits but that no test asserted:
- allowed_roots normalisation, including that a root normalising to the
empty string is dropped rather than kept (an empty root is a prefix of
every path, so keeping one would silently open the whole repository)
- api_base, user_agent and timeout actually reaching the request
- an empty response body parsing as {} rather than raising
- an error body that is not GitHub's JSON envelope falling back to the
status, without the gateway's own body being quoted back
- get_file refusing a non-base64 envelope, and replacing undecodable
bytes rather than raising
- the unmanaged-path refusal carrying status 400 and naming the boundary
- a trees entry with no size listing as 0
FakeGitHub gains `timeouts` and a mutable `tree`, matching the mutable
state it already exposes for branches and pull requests.
Each new test was mutation-checked against the source it covers: nine
targeted mutations to client.py were each detected by at least one of
them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review follow-ups: - The empty-response-body test relied entirely on the absence of an exception. It now also asserts the ref that went out, so a reader can see what the call was supposed to do and not only that it survived. - The non-JSON error fixture passed msg="Not Found" alongside a 502 HTML body, which contradicted itself on the HTTPError object even though nothing asserted on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
Self-review rubricScored adversarially against the diff and against command output, not against judgement. Universal
Repo-specific
Two items that were scored FAIL first and then fixedBoth were mechanical, so both were fixed in
Judgement calls left standing, with reasons
One observation from outside the diff
This review comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). drafted by Claude on behalf of Daniel Stephenson |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Python client's uncovered branches are covered by characterisation tests. No production code is changed, and no existing assertion is weakened.
Fifteen tests are added to
python/tests/test_client.py, locking in behaviour that every public method inherits but that nothing asserted:allowed_rootshas its slashes and whitespace tidied, and a root that normalises to the empty string is dropped rather than kept. That last one is load-bearing: an empty root is a prefix of every path, so keeping one would silently open the whole repository.Noneand()are confirmed not to be collapsed into each other.api_base(with and without a trailing slash),user_agent, andtimeoutare each confirmed to reach the outgoing request. The timeout case is asserted across every call a save makes, not just the first, because a request without a deadline hangs a request thread rather than failing visibly.{}rather than raising, which several GitHub endpoints require.GitHub API returned HTTP <code>and carries the status, and the gateway's own body is not quoted back into the message.get_filerefuses a non-base64envelope rather than decoding it into silent nonsense, and replaces undecodable bytes rather than raising.status=400and to name the boundary, and to say "outside the repository" when no roots are configured. A path equal to a root is confirmed to be inside it.sizelists as0rather than failing the whole call.FakeGitHubgains atimeoutslist and a mutabletree, matching the mutable state it already exposes for branches and pull requests. The literal tree that was inlined in the handler is moved to__init__unchanged.Which half
python/only. Nothing underjs/is touched.Test plan
PYTHONPATH=src python3 -m unittest discover -s tests -v— 46 tests, all passing (31 before this change). Executed locally on Python 3.8; the CI matrix runs the 3.9–3.13 floor-to-ceiling.python/src/github_docs/client.pyone at a time, and each was detected by at least one of the new tests: the empty-body guard, thesizedefault, thetimeoutkwarg, theapi_baserstrip, the base64 encoding guard, theerrors="replace"decode, the root-normalisation filter, thestatus=400on the path refusal, and the HTTP-status fallback message.client.pywas restored byte-for-byte afterwards; the diff contains one file.jslegs cannot be run locally (nonodein this environment) and are left to CI. This change touches no file underjs/.Invariants
dependencies = []is untouched; no non-stdlib import is added.TOKENsentinel is unchanged.Issues
No tracking issue — this is a Stage B test-expansion cycle. A full documentation-accuracy sweep was run first across all seven sources of truth and found no drift, so that work mode was not selected.
The two open issues, #5 and #6, were both deferred this cycle for the same reason: neither can be resolved by a code change. #5 needs a publish decision plus an npm organisation secret, and #6 needs a pending publisher registered on PyPI and a decision about the distribution name — all operator actions on systems outside this repository. It is worth recording against #6 that its first listed prerequisite, a
dry_runinput onrelease-pypi.yml, has since been satisfied by #7.This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).
drafted by Claude on behalf of Daniel Stephenson