From 59c214b2c6ba262ef6428fe360444d1507525ed8 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Tue, 13 Oct 2026 18:40:55 +0000 Subject: [PATCH 1/2] Make the repoG#8 assertion able to fail, give lanes' stderr filter its -e, and stop counting the bash files in prose (#66, #54, #58) #66: `hasnt` is a fixed-string `case`, so the `.*` in the repoG#8 "no separate CLAIMED line" check was two literal characters and the assertion could never fail. It is now a `grep -cE` over the log, the shape the repoK#5 sibling already had, asserting a count of 0. Proved able to fail: with a CLAIMED line injected into the log the old assertion still passed and the new one reports expected [0], got [1]. #54 part a: `lanes`' local-read stderr filter was `grep -v 'pattern' -- file`. With the pattern a bare operand, a getopt that does not permute (BSD's, and GNU's under POSIXLY_CORRECT) reads `--` as a second FILE. It is now `grep -v -e 'pattern' -- file`, with a fence test beside the restart skill's that runs `lanes` under POSIXLY_CORRECT=1 against a stub that prints the notice and another stderr line. Part b, the quote-aware checker in tests/test_repo_hygiene.py, waits for #195. #58, the in-bounds sites: the README's Windows sentence and macOS parse sentence, the macOS job's comment in tests.yml (and the Windows job's), the lane manual's sandbox paragraph and the suite's own header and bin-directory comments said "four" or "five" about files that are sixteen in the macOS parse step today. They now carry no number. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/tests.yml | 4 ++-- README.md | 10 ++++---- docs/README-lanes.md | 2 +- lanes | 8 ++++++- tests/test_lane_helpers.sh | 47 +++++++++++++++++++++++++++++++++---- 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e27bc64a..256e72bf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -150,7 +150,7 @@ jobs: # THE SAME SUITE ON WINDOWS, because a person on Windows still CLONES this # repository and reads it. What this job proves is the checkout — that the - # `.gitattributes` rule holds and the three bash files are LF in the index — + # `.gitattributes` rule holds and the bash files are LF in the index — # and the hygiene tests, which need no shell. The estate and installer tests # carry `WINDOWS_SKIP` in `tests/conftest.py`: these files are bash, there is # no PowerShell twin to run instead, and on Windows the way in is WSL2. @@ -271,7 +271,7 @@ jobs: /bin/bash -n tests/test_lane_helpers.sh /bin/bash -n tests/run.sh - # THE SAME SUITE ON macOS, because these four files are what a Mac runs. + # THE SAME SUITE ON macOS, because the shipped bash files are what a Mac runs. # This is the job openRepoShape's own macOS `/bin/bash -n` step existed for # before the split above, and it moved here with the files. Measured at # 49-54 minutes against Linux's 17-22, it no longer runs on every push: diff --git a/README.md b/README.md index 18c8f7e6..493b7734 100644 --- a/README.md +++ b/README.md @@ -407,8 +407,8 @@ https credential prompt fails and is reported; an ssh host-key or passphrase prompt is ssh's own and still blocks, so run `--fetch` where ssh is already non-interactive. -Windows: all four files are bash, and there is no PowerShell twin. On Windows -the way in is WSL2, exactly as it is for openRepoShape's `setup.sh`. +Windows: no command here has a PowerShell twin. On Windows the way in is WSL2, +exactly as it is for openRepoShape's `setup.sh`. ## The dependency direction @@ -477,9 +477,9 @@ refuses. CI runs the suite on ubuntu, windows and macOS, plus one ubuntu job that checks out WITHOUT submodules to prove the skip path exits 0. The macOS job parses -each of the four bash files with `/bin/bash -n` — bash 3.2, the last GPLv2 -release and what Apple still ships — because that is the claim the suite itself -cannot make. +every bash file this repository ships with `/bin/bash -n` — bash 3.2, the last +GPLv2 release and what Apple still ships — because that is the claim the suite +itself cannot make. ## Licence diff --git a/docs/README-lanes.md b/docs/README-lanes.md index 7c0d16ca..9aec6f64 100644 --- a/docs/README-lanes.md +++ b/docs/README-lanes.md @@ -594,7 +594,7 @@ under, and it is the COUNT and never `pgrep`'s exit status that decides. `tests/run.sh` is the canonical implementation of this guard — the lines above are it in one place, for a person with no checkout in front of them. -The suite copies the four commands and the shipped alias table into a sandbox +The suite copies the lane commands and the shipped alias table into a sandbox BIN DIRECTORY, seeds a workspace repository with nothing but data in it, and writes that sandbox's own `workspace.yaml` to join the two — which is the world after Amendment 9, exactly. It unsets every `LANES_*` seam first, so diff --git a/lanes b/lanes index 9d54f7ca..690a46ce 100755 --- a/lanes +++ b/lanes @@ -517,7 +517,13 @@ else local_err_file="$(mktemp "${TMPDIR:-/tmp}/lanes-local.XXXXXX" 2>/dev/null || printf '')" if [ -n "$local_err_file" ]; then out="$("$LANES_EDIT" lanes ${idx_args[@]+"${idx_args[@]}"} ${args[@]+"${args[@]}"} 2>"$local_err_file")" || rc=$? - grep -v 'not fetching; reading origin/' -- "$local_err_file" >&2 || : + # `-e` BEFORE THE `--` (#54, the shape #53 fixed at `skills/restart/SKILL.md`): + # with the pattern as a bare operand a getopt that does not permute — + # BSD's, so every macOS shell, and GNU's under `POSIXLY_CORRECT` — takes + # the pattern for the first operand and reads `--` as a second, a FILE it + # cannot open. `-e` makes the pattern an option's argument, so `--` ends + # the options where it says it does. + grep -v -e 'not fetching; reading origin/' -- "$local_err_file" >&2 || : rm -f -- "$local_err_file" else # No capture file, so nothing is filtered and everything reaches the diff --git a/tests/test_lane_helpers.sh b/tests/test_lane_helpers.sh index 53e5d3c1..8e35a479 100755 --- a/tests/test_lane_helpers.sh +++ b/tests/test_lane_helpers.sh @@ -27,10 +27,10 @@ # stops the call. # # WHERE THE FILES ARE, after lane-collision-protocol Amendment 9's move. The -# four commands and the shipped alias table live at the ROOT of +# lane commands and the shipped alias table live at the ROOT of # `opensoft/openRepoTools` and are INSTALLED into a bin directory; the person's # workspace repository holds data and no code at all. This suite reproduces -# exactly that: it copies the five files into a sandbox bin directory, seeds a +# exactly that: it copies those files into a sandbox bin directory, seeds a # workspace repository with nothing but data in it, and writes the sandbox's own # `$AGENT_PROTOCOL_ROOT/workspace.yaml` to join the two — which is the default # resolution under test, since `:55` unsets every LANES_* seam so that the @@ -490,7 +490,7 @@ STALE_H=4 # lanes-edit.sh's default, Rule 1's four hours # ------------------------------------------------------------- the register -# THE BIN DIRECTORY — the post-move world, where `--install` places the four +# THE BIN DIRECTORY — the post-move world, where `--install` places the lane # commands and the shipped alias table as regular files at 755. The workspace # repository below gets NONE of them: Amendment 9(a), data only. export OPENREPOTOOLS_BIN_DIR="$SANDBOX/bin" @@ -1512,7 +1512,13 @@ is "--force on a stale claim takes it over" "$rc" 0 G2="$(cat "$LOGD/repoG-2.md")" has "…writing a TAKEOVER line" "$G2" "TAKEOVER — lane repoG-2, session " has "…referencing the claim it supersedes with ←" "$G2" "opensoft/repoG#8 ← lane:repoG-1" -hasnt "…and no separate CLAIMED line: TAKEOVER is itself an open verb" "$G2" "CLAIMED — lane repoG-2, session .*opensoft/repoG#8" +# A REAL PATTERN, NOT `hasnt`'S FIXED STRING (issue #66): `hasnt` is a `case` +# containment test, so the `.*` this line used to carry to skip the session uuid +# was the two literal characters, never a wildcard — and a log that DID hold a +# separate CLAIMED line for repoG#8 passed it all the same. `grep -cE` over the +# log is the real thing, and it counts the lines, so it can be 1. +is "…and no separate CLAIMED line: TAKEOVER is itself an open verb" \ + "$(printf '%s\n' "$G2" | grep -cE '^CLAIMED — lane repoG-2,.*opensoft/repoG#8')" 0 run "$E" who "opensoft/repoG#8" has "…so the taker now holds it" "$out" "HOLDS lane repoG-2" @@ -4900,6 +4906,39 @@ is "…the other line reaches the person UNPREFIXED, exactly as the helper wro hasnt "…and grep itself never errors on \`--\` as though it were a missing file" \ "$(cat "$SANDBOX/fence3b.err")" "No such file or directory" +# #54 part a — `lanes`' OWN LOCAL READ HAD THE SAME LINE (#53 fixed the one in +# the file above, and `lane`'s copy already carries the `-e`; this was the last +# one without it). Its stderr is kept minus the one no-fetch sentence, through +# `grep -v 'pattern' -- file`, and with the pattern a bare operand a +# non-permuting getopt reads `--` as a second FILE. The stub says the notice +# AND another line, the way the helper does when this workstation's session +# records are unreadable, and exits 8 — "no lane" — which is the shortest way +# through the command that still reaches the filter. `POSIXLY_CORRECT=1` is the +# same stand-in for BSD's `grep` the case above uses: measured against the +# unfixed line, this case printed `grep: --: No such file or directory` and put +# the temp file's own name in front of the real line. +cat > "$SANDBOX/lanesfence" <<'WRAP' +#!/usr/bin/env bash +case "${1-}" in + lanes) + printf 'LANES_NO_FETCH=1 — not fetching; reading origin/main as the ref already stands here\n' >&2 + printf 'session records for this workstation could not be read: permission denied\n' >&2 + exit 8 ;; +esac +exit 0 +WRAP +chmod +x "$SANDBOX/lanesfence" +lanes_fence_rc=0 +( cd -- "$SANDBOX" && POSIXLY_CORRECT=1 LANES_EDIT="$SANDBOX/lanesfence" \ + "$OPENREPOTOOLS_BIN_DIR/lanes" /dev/null 2>"$SANDBOX/lanesfence.err" ) || lanes_fence_rc=$? +is "lanes under a NON-PERMUTING getopt (BSD's shape) still answers, \"no lane\" being its 8" "$lanes_fence_rc" 8 +is "…and the other stderr line reaches the person UNPREFIXED, exactly as the helper wrote it" \ + "$(cat "$SANDBOX/lanesfence.err")" "session records for this workstation could not be read: permission denied" +hasnt "…grep itself never errors on \`--\` as though it were a missing file" \ + "$(cat "$SANDBOX/lanesfence.err")" "grep: --:" +hasnt "…and the known no-fetch notice is still dropped from it" \ + "$(cat "$SANDBOX/lanesfence.err")" "not fetching" + # AND WITH NO CAPTURE FILE AT ALL, NOTHING IS FILTERED — silence in place of a # stream this fence could not open would be the same defect the notice fix # exists to correct, only quieter. From 08dd70467ace84eed6e7a07212d002d01bd96ef9 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sat, 17 Oct 2026 20:59:20 +0000 Subject: [PATCH 2/2] Say the shipped files without a count in the installer's PATH note, the install tests' docstrings, the pin reader, the CI runner note and the lane manual (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With #191 and #227 on main the rest of #58's sites can be reached, and two sentences found by the sweep were stale in more than their number. - `openRepoTools` and `tests/test_openrepotools_command.py`: "ONE PATH NOTE for the four of them", "all four or none", "which of the four were missing", "each of the four files `--install` places" and "each of the ten" said a number the installer has not placed for a long while. They now say "everything it placed", "all of them" and "each of them". Comments and docstrings only. - `.github/workflows/tests.yml`: "Nothing this repository ships is Python at all — three bash files —" was wrong twice. `lanes-index` and `lane-worktrees` are `python3` scripts, and the count was stale. The true claim, the one the line exists for, is that nothing shipped needs a Python package: both import only the standard library. - `tests/test_upstream_pin.py`: the pin reader's docstring said the repository "ships three bash files and installs nothing"; it now says what is true without a count. - `docs/README-lanes.md`: the `--install` comment's "thirteen commands". Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/tests.yml | 6 ++++-- docs/README-lanes.md | 2 +- openRepoTools | 2 +- tests/test_openrepotools_command.py | 17 +++++++++-------- tests/test_upstream_pin.py | 11 ++++++----- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3c7caa44..ee692640 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -87,8 +87,10 @@ jobs: python-version: '3.x' # pytest is the TEST RUNNER and is the only thing installed here. - # Nothing this repository ships is Python at all — three bash files — - # and the suite itself imports only the standard library. + # Nothing this repository ships needs a Python package: its commands are + # bash, or `python3` (`lanes-index`, `lane-worktrees`) importing only the + # standard library, and the suite itself imports only the standard + # library. # PINNED. A CI job that resolves "latest" on every run is a job whose # result depends on the day it ran, which is the same objection this # family makes to pinning a tag instead of a commit. diff --git a/docs/README-lanes.md b/docs/README-lanes.md index 9aec6f64..c733365e 100644 --- a/docs/README-lanes.md +++ b/docs/README-lanes.md @@ -4072,7 +4072,7 @@ machine whose repository already exists it clones it, adds nothing to a seeded repository, writes the pointer file and runs `link-estates`: ```sh -openRepoTools --install # the thirteen commands, the three skills, the three command files and the hook entry +openRepoTools --install # the commands, the three skills, the three command files and the hook entry openRepoTools wip init # create or adopt the workspace, and link it ``` diff --git a/openRepoTools b/openRepoTools index 7aa697ce..08a98962 100755 --- a/openRepoTools +++ b/openRepoTools @@ -1936,7 +1936,7 @@ install_commands() { # which is the population the marker exists for anyway. receipt_record retire_commands - # ONE PATH NOTE for the four of them: the directory is the same one. + # ONE PATH NOTE for everything it placed: the directory is the same one. case ":${PATH:-}:" in *":$dir:"*) ;; *) diff --git a/tests/test_openrepotools_command.py b/tests/test_openrepotools_command.py index c254957e..0bdc7683 100644 --- a/tests/test_openrepotools_command.py +++ b/tests/test_openrepotools_command.py @@ -2138,8 +2138,9 @@ def test_bin_dir_overrides_where_it_lands(tmp_path): @NEEDS_JQ def test_install_says_how_to_put_it_on_path(tmp_path): - """ONE path note for the four of them: the directory is the same one, and - four copies of the same `export` line reads as four problems.""" + """ONE path note for everything `--install` places: the directory is the + same one, and the same `export` line once per file reads as a problem for + each of them.""" result = run_cmd("--install", home=tmp_path) assert f'export PATH="{tmp_path}/.local/bin:$PATH"' in result.stdout assert result.stdout.count("export PATH=") == 1 @@ -2198,7 +2199,7 @@ def fake_github(tmp_path, served_names) -> dict: """A fake `gh` serving exactly `served_names`, and a `curl` that refuses. Factored out of the fixture so one test can WITHHOLD a file: `--install` - places all four or none, and the only way to prove "or none" is a server + places all of them or none, and the only way to prove "or none" is a server that cannot answer for one of them. """ served = tmp_path / "served" @@ -2248,7 +2249,7 @@ def offline_github(tmp_path): `fetch_from_repo` tries `gh api` before the raw URL, so a `gh` that answers the calls the command makes is the whole of the server these tests need: `contents/` comes back as this checkout's own bytes for each - of the four files `--install` places. `curl` is shadowed by a script that + of the files `--install` places. `curl` is shadowed by a script that exits 1 — belt and braces, so that a fake `gh` which stopped matching could never quietly become a real request to raw.githubusercontent.com. """ @@ -2260,7 +2261,7 @@ def test_install_places_all_nine_or_none(tmp_path): on its #83). One file at a time, dying on the first fetch that failed, leaves a person with a NEW `openRepoTools` and no `park` — a half-install that prints `installed at` and is not one — with nothing on screen to say - which of the four were missing. + which of them were missing. Run from stdin with `park` withheld: NOTHING is placed, nothing already there is replaced, and the refusal names the file it could not fetch. @@ -2335,9 +2336,9 @@ def test_install_from_stdin_fetches_itself_into_a_live_workdir(offline_github, bash -s -- --install`. Run from stdin there is no file to copy from — not for this command, not - for its three siblings and not for the four lane helpers, the alias table - or the skill — so `install_commands` fetches each of the ten at this ref - into a temporary directory. THAT DIRECTORY HAS TO STILL BE THERE: + for its siblings and not for the lane helpers, the alias table or the + skill — so `install_commands` fetches each of them at this ref into a + temporary directory. THAT DIRECTORY HAS TO STILL BE THERE: `workdir()` sets its EXIT trap in the main shell rather than inside a `$(...)` subshell, whose trap would fire the instant the substitution closed and take the directory away before a single fetched byte had landed diff --git a/tests/test_upstream_pin.py b/tests/test_upstream_pin.py index 109dcb8f..8a3c9b8c 100644 --- a/tests/test_upstream_pin.py +++ b/tests/test_upstream_pin.py @@ -52,11 +52,12 @@ def pin_scalars() -> dict: """Every `key: value` in the pin file, at any indent, comments stripped. A ~15-line reader rather than a YAML dependency: this repository ships - three bash files and installs nothing, and the file it reads is a flat map - with one nested key written by hand. The nesting is flattened onto the leaf - name (`tree_sha256`), which is unambiguous here because no leaf name - repeats — asserted below, so a second `tree_sha256` under some other block - would fail rather than shadow this one. + bash and standard-library `python3` commands and installs no package, and + the file it reads is a flat map with one nested key written by hand. The + nesting is flattened onto the leaf name (`tree_sha256`), which is + unambiguous here because no leaf name repeats — asserted below, so a + second `tree_sha256` under some other block would fail rather than shadow + this one. """ values: dict = {} for raw in PIN.read_text(encoding="utf-8").splitlines():