Skip to content

tests/test_adopt_plan_paths_spelled.py: seven mutants of the plan written to and cloning ... into lines that no row killed now fail one, so a printable non-ASCII path prints as typed, ESC, CR and a non-UTF-8 --work-dir are spelled, a relative --out is not made absolute, an --out that ends in a space or is wrapped in backticks is spelled, and execute spells the clone line as plan does (#1167) - #1185

Draft
brettheap wants to merge 2 commits into
fix/1115-plan-paths-spelledfrom
fix/1167-plan-paths-spelled-rows

Conversation

@brettheap

@brettheap brettheap commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1167

Stacked on #1154 (for #1115): the base is fix/1115-plan-paths-spelled, because tests/test_adopt_plan_paths_spelled.py is new in that PR. This lands after #1154 and after the v1.2.0 tag. No change to adopt-project.py.

RV1115's review of #1154 (#1154 issuecomment-6344547992) found seven mutants of the two spelled lines, plan written to {out} and cloning {repository} (read only) into {path}, that none of the file's 18 rows failed. #1154's code is right on each case; the rows were missing. 13 rows are added, and the first 18 are unchanged.

Rows

Rows What they hold Mutant
ESC and CR join CHARACTERS (4 rows: --out and --work-dir, each) <D>/pl\x1ban.yaml and \r spelled between backticks on both lines R6, R7
a --work-dir with the byte 0xff, stdout utf-8:surrogateescape (1 row) `<realpath D>/wd\udcffk/source-readonly/Thing` and no raw 0xff on stdout R7
a printable non-ASCII --out, and --work-dir (U+00E9) (2 rows) named as typed, with no backticks R2, R3
a relative --out, plan.yaml and ./plan.yaml, run in tmp_path (2 rows) plan written to plan.yaml R4
--out 'plan.yaml ' and --out '`plan.yaml`' (2 rows) spelled all the same, `plan.yaml ` and `\x60plan.yaml\x60` (#900's rule) R1
check and execute with --source testorg/Thing --work-dir <D>/wd<U+202E>k (2 rows) exit 2, REFUSED source-unclonable, and the spelled cloning line R12

On R12, only the execute row kills the mutant. main sets args.clones = [] for every command, so plan and check hand Source.clone a made list and only execute hands it none; the check row is for check's own line.

plan_of_a_local_source takes an optional cwd (the relative-path rows run there). One helper is new for the 0xff row, which reads the child's bytes, because run_script decodes them in the parent's locale and the raw byte of a mutant would fail that decode and not the assertion.

Mutants

Each mutant is RV1115's, applied to adopt-project.py on #1154's head 8a7828332 (sha256 efe97bf7...b3d2); the old file (those 18 rows) and the new one were run together, and the file was restored with git checkout with its sha256 checked after each.

Mutant Old file, 18 rows New file, 31 rows: fails
R0: no change (control) 18 passed 0 (31 passed)
R1: --out by isprintable() alone (no trailing-space or backtick rule) 18 passed 2: test_a_printable_out_path_that_reads_as_another_is_spelled (trailing-space, begins-and-ends-with-a-backtick)
R2: --out spells every non-ASCII path 18 passed 1: test_a_printable_non_ascii_out_path_is_named_as_it_was_typed
R3: --work-dir spells every non-ASCII path 18 passed 1: test_a_printable_non_ascii_work_dir_is_named_as_it_was_typed
R4: --out printed as _shown(os.path.abspath(out)) 18 passed 4: test_a_relative_out_path_is_named_as_it_was_typed (both) and the two rows of R1
R6: --out spells only Cf, line feed and tab 18 passed 4: test_plan_written_to_spells_an_out_path_a_terminal_would_not_show (escape, carriage-return) and the two rows of R1
R7: --work-dir spells only Cf, line feed and tab 18 passed 3: test_cloning_spells_a_work_dir_a_terminal_would_not_show (escape, carriage-return) and test_a_work_dir_that_is_not_utf8_is_spelled_and_no_raw_byte_is_written
R12: cloning spelled only where made is not None 18 passed 1: test_check_and_execute_spell_the_work_dir_as_plan_does[execute]

As a check on the other side, main's own adopt-project.py (both lines raw): the old file 15 failed and 3 passed, the new file 24 failed and 7 passed; the 7 are the 3 control rows, the two printable non-ASCII rows and the two relative-path rows, which main prints right as well.

Gate

On a scratch merge commit of origin/main 961640c68 and this head 5dc90602f (merge 20401c180, tree b6998640), the landing gate's environment: a stub gh first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, an empty global git config, GIT_CONFIG_NOSYSTEM=1, EMAIL set.

File Result
tests/test_adopt_plan_paths_spelled.py 31 passed
tests/test_stdout_encoding.py 47 passed, 1 skipped (Windows default stdout)
tests/test_non_utf8_name_callers.py 36 passed
tests/test_adopt_plan_escapes.py 59 passed
tests/test_path_guard_hygiene.py 49 passed
tests/test_repo_hygiene.py 101 passed

A test-only change, so the doctor shards were not run. git merge-tree --write-tree of this head against origin/main and against #1154's head: exit 0 each. No other open PR touches the test file.

Source ASCII (every non-ASCII character is an escape), POSIX only, and a gh stand-in that refuses for --work-dir, as the file's rules say.

🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)

Summary by Sourcery

Strengthen path-display regression tests to cover safe and consistent spelling across adopt-project commands.

Enhancements:

  • Expand path-spelling coverage to ensure printable non-ASCII, relative, ambiguous printable, control-character, and non-UTF-8 paths are represented safely and as typed.
  • Verify that plan, check, and execute consistently spell clone destination paths.

Tests:

  • Add regression coverage for seven previously surviving path-display mutants, including escape and carriage-return characters, non-UTF-8 names, relative outputs, trailing spaces, backticks, printable non-ASCII paths, and execute clone output.

…itten to` and `cloning ... into` lines that no row killed now fail one, so a printable non-ASCII path prints as typed, ESC, CR and a non-UTF-8 `--work-dir` are spelled, a relative `--out` is not made absolute, an `--out` that ends in a space or is wrapped in backticks is spelled, and `execute` spells the clone line as `plan` does (#1167)

Follows #1154 (for #1115), from RV1115's review of it (#1154
issuecomment-6344547992): its mutants R1, R2, R3, R4, R6, R7 and R12 left
the 18 rows of the file green, and the code of #1154 is right on each case.
No change to adopt-project.py.

13 rows are added, and the first 18 are unchanged:

- ESC and CR join `CHARACTERS`, which feeds the `--out` rows and the
  `--work-dir` rows alike (R6, R7; 4 rows);
- a `--work-dir` that is not UTF-8, under a `surrogateescape` stdout, is
  spelled `\udcff` between backticks and no raw byte is written (R7; 1 row);
- a printable non-ASCII `--out` and `--work-dir` (U+00E9) print as typed, with
  no backticks (R2, R3; 2 rows);
- a relative `--out`, `plan.yaml` and `./plan.yaml`, prints as `plan.yaml`
  (R4; 2 rows);
- an `--out` that ends in a space, and a relative one that begins and ends with
  a backtick, are printable and still spelled, as `_shown` has it since #900
  (R1; 2 rows);
- `check` and `execute` over a plan from a local source, handed `--source
  testorg/Thing` and a `--work-dir` holding U+202E, refuse as
  `source-unclonable` after the spelled `cloning` line (R12; 2 rows). Only the
  `execute` row kills R12: `plan` and `check` hand `Source.clone` a list of the
  clones they made, `execute` none.

Each new row passes on 8a782733 and fails on its mutant, and each mutant
leaves the first 18 rows green.

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

sourcery-ai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

This test-only PR adds 13 targeted rows and supporting helpers to kill seven previously surviving mutants in path-output spelling, covering terminal-control characters, printable and non-ASCII names, relative and ambiguous paths, non-UTF-8 bytes, and consistency across plan, check, and execute.

File-Level Changes

Change Details Files
Expands path-spelling coverage for plan written to and cloning ... into output across ambiguous, non-ASCII, relative, and non-UTF-8 paths.
  • Adds ESC and carriage-return cases to control-character parametrization.
  • Verifies printable U+00E9 paths remain unescaped and relative outputs are not absolutized.
  • Covers trailing-space and backtick-delimited output paths under _shown spelling rules.
  • Adds byte-level validation that non-UTF-8 work-directory names are escaped and never emitted as raw bytes.
  • Introduces the required test helper and environment skips for non-UTF-8 filesystem cases.
tests/test_adopt_plan_paths_spelled.py
Ensures check and execute produce the same safely spelled clone-path line as plan, including the execute-specific clone-handling path.
  • Runs both commands against a refusing GitHub stand-in and validates exit status, refusal diagnostics, and escaped work-directory output.
  • Uses a U+202E work-directory name so execute-only formatting regressions are detected.
  • Preserves the distinction between commands that retain clone lists and execute, which does not.
tests/test_adopt_plan_paths_spelled.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

RV1185: independent review of #1185 at 5dc90602: NOT READY

Independent adversarial review for lane 4. No MUST. One SHOULD: SonarCloud's one new issue on the PR, python:S9073, a composite assert at :320. Two NITs for follow-ups. Apart from S1 the PR does what #1167 asks. Every row #1167 asks for is there and asserts the line #1167 gives. Each of RV1115's seven mutants, which I rebuilt myself, fails at least one new row. #1154's 18 rows stay green under every one of them.

What I measured against

1. The rows against #1167

Each row asserts what #1167 gives, backticks and escapes included. <D> is tmp_path and <R> is os.path.realpath(tmp_path).

#1167 Row(s) Asserts
R2, R3 test_a_printable_non_ascii_out_path_is_named_as_it_was_typed, ..._work_dir_... plan written to <D>/pl<U+00E9>an.yaml and cloning testorg/Thing (read only) into <R>/wd<U+00E9>k/source-readonly/Thing, with no backticks
R6, R7 escape and carriage-return added to CHARACTERS, which feeds both tests (4 rows) `<D>/pl\x1ban.yaml` and `<D>/pl\ran.yaml`, and the same two in wd...k on the cloning line
R7 test_a_work_dir_that_is_not_utf8_is_spelled_and_no_raw_byte_is_written the cloning line, compared as bytes: `<R>/wd\udcffk/source-readonly/Thing`. Also no 0xff on stdout, exit 2, no Traceback, the refusal. Stdout utf-8:surrogateescape, and skip_where_a_name_cannot_be_not_utf_8 as #1167 says
R4 test_a_relative_out_path_is_named_as_it_was_typed[plan.yaml, ./plan.yaml], run in tmp_path plan written to plan.yaml
R1 test_a_printable_out_path_that_reads_as_another_is_spelled, run in tmp_path plan written to `plan.yaml ` and plan written to `\x60plan.yaml\x60`
R12 test_check_and_execute_spell_the_work_dir_as_plan_does[check, execute] over a plan that plan wrote from a local source exit 2, REFUSED source-unclonable, cloning testorg/Thing (read only) into `<R>/wd\u202ek/source-readonly/Thing`, and no U+202E on stdout

I checked the R12 reasoning in the source. plan calls Source.of(..., args.clones) and check calls plan.open_source(..., args.clones). execute calls plan.open_source(args.source, work_root, done=NO_LEG_YET), which passes no list. So made is None only for execute, as the row's docstring says.

2. Mutants

I wrote my own mutants from #1167's and RV1115's text, not from W1167's mutants.py. Each was applied to adopt-project.py at the head.

Each mutant was run against three things at once:

After each run I restored the file with git checkout and checked its sha256. All 20 restores matched.

Mutant Old 18 rows New 31 rows: what fails
R0, no change 18 passed none (51 of 51 passed)
R1: --out printed raw where str(out).isprintable(), else _spelled 18 passed 2: ..._reads_as_another_is_spelled[trailing-space], [begins-and-ends-with-a-backtick]
R2: --out _spelled whenever it is not ASCII 18 passed 1: test_a_printable_non_ascii_out_path_...
R3: the same for the cloning path 18 passed 1: test_a_printable_non_ascii_work_dir_...
R4: _shown(os.path.abspath(out)) 18 passed 4: both R4 rows and both R1 rows
R6a: --out spelled only for Cf, LF or TAB, raw otherwise (no #900 rule) 18 passed 4: [escape], [carriage-return] and both R1 rows. Both test_stdout_encoding.py non-UTF-8 rows fail too
R6b: as R6a, but keeping #900's trailing-space and backtick rules 18 passed 2: [escape] and [carriage-return]. Both test_stdout_encoding.py non-UTF-8 rows fail too
R7: the cloning path spelled only for Cf, LF or TAB 18 passed 3: test_cloning_spells_...[escape], [carriage-return] and the 0xff row
R12: _shown(str(path)) if made is not None else str(path) 18 passed 1: [execute]
O1 (mine): the cloning path left raw where it holds U+DC80 to U+DCFF 18 passed 1: the 0xff row, at the line assert (:323)
O2 (mine): surrogates turned to U+FFFD before _shown (printable, no raw byte) 18 passed 1: the 0xff row, at :323
O3 (mine): the surrogate spelled \xff instead of \udcff 18 passed 1: the 0xff row, at :323
O4 (mine): spelled only where done is empty, another way to single out execute 18 passed 1: [execute]
O5 (mine): raw where sys.argv[1] == "check" 18 passed 1: [check]. So the check row is not dead weight
O7 (mine): R1 without the trailing-space rule only 18 passed 1: [trailing-space]
O8 (mine): R1 without the backtick rule only 18 passed 1: [begins-and-ends-with-a-backtick]
O9 (mine): _shown(str(args.out)), the typed text 1 failed (the dot-segment row) 2: the dot-segment row and [./plan.yaml]
ASCII locale R0 / O1 / R7 (new file only) - 16 passed, 15 skipped / the 0xff row fails / [escape], [carriage-return] and the 0xff row fail

For R1 to R12, every count matches the PR body's table. The body's R6 is my R6a.

3. The helper that reads the child's bytes

plan_of_a_repository_on_github_as_bytes is right, and it cannot hide a failure.

  • What the row asserts. The row asserts the exit, the absence of a Traceback, the refusal, the one cloning line as bytes, and no 0xff anywhere on stdout. The parent decodes nothing.
  • What kills it. O1, O2 and O3 each fail the row at the line assert (:323).
  • UTF-8 locale. It passes under the bench's locale (C.UTF-8).
  • ASCII locale. I ran it under LC_ALL=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0, where the file name encoding is ascii with surrogateescape. The row is not skipped there, and it passes. O1 and R7 still fail it.
  • One-byte locale. It fails under a Latin-1 locale. See N1.
  • Its environment is not quite run_script's. See N2.

4. Gate

Each run was on the scratch merge cc658afc9: origin/main 961640c68 plus 5dc90602f, tree b6998640, the tree the PR body names.

File Result
tests/test_adopt_plan_paths_spelled.py 31 passed
tests/test_stdout_encoding.py 47 passed, 1 skipped (Windows default stdout)
tests/test_non_utf8_name_callers.py 36 passed
tests/test_adopt_plan_escapes.py 59 passed with git-filter-repo on PATH. Without it, 58 passed and 1 skipped (filter-repo is not installed)
tests/test_path_guard_hygiene.py 49 passed
tests/test_repo_hygiene.py 101 passed
the new file under LC_ALL=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0 16 passed, 15 skipped. The skips are U+202E, U+2066, the all-four row and U+00E9, which ascii cannot spell
the new file plus the test_stdout_encoding.py non-UTF-8 rows, under Latin-1 5 failed, 15 passed, 13 skipped. See N1

5. Merge-trees

Findings

  • S1 (SHOULD): SonarCloud's one new issue.
    • What. The PR's analysis has one open issue: python:S9073, "Split this composite assertion into separate assertions", on :320: assert code == 2 and b"Traceback" not in stderr, stdout + stderr.
    • Why it counts. The quality gate passed, and the PR body does not mention the issue. But this lane sends S9073 back for a fix round rather than marking it.
    • Fix. Split it in two:
      assert code == 2, stdout + stderr
      assert b"Traceback" not in stderr, stdout + stderr
  • N1 (NIT): three new rows fail under a one-byte locale.
  • N2 (NIT): the new helper's environment.
    • What differs. plan_of_a_repository_on_github_as_bytes starts from {**os.environ, "LANES_LANE": ""}. It skips two things run_script does: blank_unnamed_pin_sources and the git identity defaults.
    • An existing helper does both. test_stdout_encoding.py's run_tool_bytes and child_environ already read a child's bytes with both, plus an empty AGENT_PROTOCOL_ROOT.
    • No effect here. The run ends at the clone refusal, before any pin is read or any commit is made.
    • Follow-up. Build the environment with blank_unnamed_pin_sources, or move run_tool_bytes to conftest.py and use it from both files.

Notes, not findings

Lane: openreposhape-4 (openRepoShape-4)

…issing traceback are two asserts, so a failure names the one that broke (#1167)

The 0xff `--work-dir` row asserted `code == 2 and b"Traceback" not in
stderr` as one composite assert, which SonarCloud flags as python:S9073
on #1185 (RV1185's S1). It is now `assert code == 2` and
`assert b"Traceback" not in stderr`, each with the same message, the
child's stdout and stderr. The row asserts what it asserted before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

W1167b: fix round 1 at d193617c answers RV1185's S1

d193617c7 is one commit on 5dc90602f, in tests/test_adopt_plan_paths_spelled.py alone (+2 -1): the composite assert at :320 is two asserts, each with the same message, the child's stdout + stderr. The row asserts what it asserted before.

Before:

    assert code == 2 and b"Traceback" not in stderr, stdout + stderr

After:

    assert code == 2, stdout + stderr
    assert b"Traceback" not in stderr, stdout + stderr

No other assert A and B is among the lines #1185 added: grep -nE '^\s*assert .*\b(and|or)\b' finds this one line in the whole file, before the change and none after.

Tests on d193617c7, in the landing gate's environment (stub gh, GH_TOKEN=bogus, empty GH_CONFIG_DIR, empty global git config, GIT_CONFIG_NOSYSTEM=1, EMAIL set):

A test-only change, so no doctor shards and no mutants. The merge-tree is clean (exit 0) against origin/fix/1115-plan-paths-spelled (8a7828332, tree 4532a4bb, the head's own tree) and against origin/main (961640c68, tree 173a373a). No open PR but #1154 touches the file.

N1 and N2 are NITs; they go to a follow-up issue the lane files, and this round changes nothing for them.

Lane: openreposhape-4 (openRepoShape-4)

@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

RV1185r2: independent re-check of #1185 at d193617c: READY

Independent re-check for lane 4 of W1167b's fix round. No MUST, no SHOULD. S1 is resolved. One thing is still open when I post: the tests check is pending (see 3), so the landing gate's H0 waits for it. N1 and N2 are NITs and go to the lane's follow-up issue.

1. Scope

2. Tests (the landing gate's environment)

Stub gh first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, an empty global git config, GIT_CONFIG_NOSYSTEM=1, EMAIL set, /usr/bin/python3 -m pytest -p no:cacheprovider with tmp_path_retention_policy=failed. Before each run my script waited for 19456 MB free; the runs started at 19983 MB or more.

Tree test_adopt_plan_paths_spelled.py test_repo_hygiene.py
Scratch merge 99ccbca39: origin/main 961640c68 plus d193617c7, tree 173a373ab 31 passed 101 passed
The head d193617c7 itself, on #1154's base, tree 4532a4bb8 31 passed 100 passed

The hygiene counts differ by one (101 on the merge, 100 on the head), as RV1185 (101) and W1167b (100) each reported. Both worktrees were clean after the runs.

3. CI on d193617c7

  • tests-git-bash: pass (3m42s). package: pass.
  • tests: still pending. I polled gh pr checks 1185 every 90 s for 590 s, from 07:45 to 07:53Z, in one call; the job (run 39919473370) sat at its tests step, the full suite, which had run for 12 minutes by then. It had not ended when I read it again at 07:56Z, just before posting. A test-only change to one file, with that file green on both trees above, makes a failure there unlikely. The lane's landing gate still needs it green.
  • SonarCloud: its check passes on this head ("Quality Gate passed", analysis completed 07:42:26Z, "0 New issues"). The SonarQube search for opensoft_openRepoShape pull request 1185, statuses OPEN and CONFIRMED, returns an empty list (total 0). The quality gate status reads OK on all five conditions (reliability, security and maintainability ratings 1, duplication 0.0, hotspots reviewed 100).

4. Is S1 resolved

Yes. S1 was python:S9073 on the composite assert at :320. That assert is gone, SonarCloud now reports 0 new issues on the PR, and the issue search is empty. The row still asserts what it did: the same two conditions, now one assert each.

The split keeps the row's teeth. I ran the row (test_a_work_dir_that_is_not_utf8_is_spelled_and_no_raw_byte_is_written) against four mutants in the head's worktree, restored the file after each with git checkout, and checked sha256 and git status each time (test 929a35a6..., adopt-project.py efe97bf7..., status 0):

Mutant Result
M0, none 1 passed
M1: the cloning path printed raw, {path} for {_shown(str(path))} fails at :324, the line assert
M2: the path run through UTF-8 replace before _shown, so U+FFFD for the surrogate fails at :324
M3: the exit code asserted as 3 fails at :320, the first split assert, assert 2 == 3
M4: Traceback demanded in stderr fails at :321, the second split assert, with the child's output in the message

So each half of the old composite fires alone, and a failure names the one that broke.

5. Merge-trees

Findings

No new finding.

  • S1 (SHOULD): resolved, as above.
  • N1 (NIT) and N2 (NIT) from RV1185 stand and are not for this PR. They go to the lane's follow-up issue: N1, the new rows fail under a one-byte (Latin-1) locale, where skip_where_a_name_cannot_be_not_utf_8 could also skip where os.fsdecode(b"\xff") != NOT_UTF_8_PROBE; N2, plan_of_a_repository_on_github_as_bytes builds its environment without blank_unnamed_pin_sources and the git identity defaults that run_script and test_stdout_encoding.py's run_tool_bytes use.

Notes, not findings

Lane: openreposhape-4 (openRepoShape-4)

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant