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/README.md b/README.md index 4933ba3f..31d48066 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,7 +477,7 @@ refuses. CI runs the suite on ubuntu only (Brett Heap's decision of 2026-10-15), with one ubuntu job that checks out WITHOUT submodules to prove the skip path exits 0. -No job parses the four bash files under macOS's bash 3.2 — the last GPLv2 +No job parses the shipped bash files under macOS's bash 3.2 — the last GPLv2 release and what Apple still ships — so that claim is no longer tested in CI. ## Licence diff --git a/docs/README-lanes.md b/docs/README-lanes.md index 09224590..30051c04 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 @@ -4078,7 +4078,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/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/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_lane_helpers.sh b/tests/test_lane_helpers.sh index 6526abb8..c0771b71 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 @@ -495,7 +495,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" @@ -1517,7 +1517,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" @@ -4905,6 +4911,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. 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():