From b6e3cdd25d15df684ae3651ef536a5a51aa90ad5 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Sat, 29 Aug 2026 17:38:41 +0100 Subject: [PATCH 1/5] A citation spelled as a path has to point somewhere Red: the guard, and 33 files that fail it. --- test/test_citations_resolve.py | 168 +++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 test/test_citations_resolve.py diff --git a/test/test_citations_resolve.py b/test/test_citations_resolve.py new file mode 100644 index 00000000..928ff480 --- /dev/null +++ b/test/test_citations_resolve.py @@ -0,0 +1,168 @@ +"""A citation spelled as a path is a pointer, so it has to point somewhere. + +Comments and documents all over this repository name the test that pins the +behaviour being described. That habit is the repository's decision-vs-accident +record: a reader who wants to know whether something is load-bearing follows the +citation and reads the guard. Retiring the Python tree (#267) deleted about +forty of the files those citations named, and the citations stayed. A pointer +into nothing is worse than no pointer, because it still reads as evidence -- the +reader is told a guard exists, goes looking, finds nothing, and cannot tell +whether the guard moved, was renamed, or never existed. + +So the rule is about the spelling rather than about tense. **A token spelled the +way a Python test file is spelled has to resolve to a file in this tree.** A +suite that retired is named without the extension, in a sentence that says it +retired -- `test_purge_ownership` (Python, retired in #267) rather than a path +somebody can try to open. That keeps the history, which is worth keeping and is +still greppable in `git log`, and it stops the history from impersonating a +live guard. + +Two rules, because there are two spellings to catch: + +- Any name shaped like a Python test module, wherever it appears: a path is + checked as written, a bare filename against every test file in the tree. A + bare name is allowed to resolve anywhere under `test/` on purpose, since a + comment saying which directory a test lives in would rot on the first move. +- Any repository-relative path into `test/` or `rust/tools/`, whatever its + extension. That second directory is gone entirely, and the fixtures under the + first moved when the Python tests that used them went. + +`CHANGELOG.md` and `docs/rust-port-scope.md` are out of scope, for the same +reason `test_docs_prose.py` excludes the archives: both are records of what was +true when they were written. A changelog entry describing a file that a later +release deleted is accurate history, and rewriting it would be editing the +record to satisfy a test. + +`docs/rust-rewrite-plan.md` is *in* scope, unlike in that test, and the +difference is what the two rules are about. Its prose is an archive, but its +divergence table is cited by row number from comments and tests throughout +`rust/` and its rows say what pins each row -- which makes those particular +sentences live pointers however old the document is. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Records rather than pointers: what they say was true when it was written. +ARCHIVAL = {"CHANGELOG.md", "docs/rust-port-scope.md"} + +# The trees a citation can be written in. Everything else (`rust/target`, a +# vendored dependency, whatever a tool drops) is not prose this repository owns. +SOURCE_TREES = (".github", "docs", "rust", "scripts", "test") +SOURCE_SUFFIXES = {".md", ".py", ".rs", ".sh", ".toml", ".yaml", ".yml"} +TOP_LEVEL_DOCS = ("AGENTS.md", "CLAUDE.md", "README.md") + +# Both patterns open on a lookbehind that refuses a match starting mid-token. +# Without it the second one reads `latest/install-guide.html` in an NVIDIA +# documentation URL as a path under `test/`, and the first reads any module +# whose name happens to end in the same letters as a citation of a shorter one. +BOUNDARY = r"(? list[Path]: + """Every file in this repository that can carry a citation. + + Globbed rather than listed. A hand-written list is a list the next file is + missing from, and a citation nothing reads is exactly the state this test + exists to end. + """ + found: list[Path] = [] + for name in TOP_LEVEL_DOCS: + page = REPO_ROOT / name + if page.is_file(): + found.append(page) + found.extend(page for page in REPO_ROOT.glob("*.toml") if page.is_file()) + for tree in SOURCE_TREES: + for path in sorted((REPO_ROOT / tree).rglob("*")): + if path.is_file() and path.suffix in SOURCE_SUFFIXES: + found.append(path) + return [ + path + for path in found + if path.relative_to(REPO_ROOT).as_posix() not in ARCHIVAL + ] + + +SOURCES = sources() + +# Every Python test file that exists, by basename, for the bare-name rule. +TEST_FILES = { + path.name for path in (REPO_ROOT / "test").rglob("test_*.py") if path.is_file() +} + + +def _resolves(citation: str) -> bool: + if "/" in citation: + return (REPO_ROOT / citation).exists() + return citation in TEST_FILES + + +def dangling(text: str) -> list[tuple[int, str]]: + """Every citation in `text` that names nothing, with the line it sits on.""" + found: list[tuple[int, str]] = [] + for number, line in enumerate(text.splitlines(), start=1): + for pattern in (TEST_MODULE, RETIRED_TREE_PATH): + for match in pattern.finditer(line): + citation = match.group(0) + if not _resolves(citation) and (number, citation) not in found: + found.append((number, citation)) + return found + + +@pytest.mark.unit +def test_the_scope_covers_the_trees_citations_are_written_in(): + """The premise the rule below rests on. + + A glob that stopped matching would leave the rule collecting no files and + reporting a clean run, which reads exactly like a run that passed. Asserted + against the shape of the repository rather than a count, so that a file + moving does not fail here for the wrong reason. + """ + assert len(SOURCES) > 100, ( + f"only {len(SOURCES)} files are in scope; the glob no longer describes " + "this repository and the rule below is checking almost nothing" + ) + for tree in SOURCE_TREES: + assert any(path.is_relative_to(REPO_ROOT / tree) for path in SOURCES), ( + f"nothing under {tree}/ is in scope, so citations written there are unchecked" + ) + assert len(TEST_FILES) > 10, ( + "the set of Python test files this rule resolves bare names against is " + f"{sorted(TEST_FILES)}, which cannot be right" + ) + for name in ARCHIVAL: + assert (REPO_ROOT / name).exists(), ( + f"{name} is excluded as a record and is not there; an exclusion that " + "names nothing hides whether the rule still has the coverage it claims" + ) + + +@pytest.mark.unit +@pytest.mark.parametrize( + "source", SOURCES, ids=lambda source: source.relative_to(REPO_ROOT).as_posix() +) +def test_every_cited_test_file_is_a_file(source): + """A citation names a guard somebody can open, or it is not written as a path. + + The fix for a failure here is one of two things and never a third. If the + behaviour is still pinned, cite the test that pins it now. If the test + retired with the Python tree, name the suite without the `.py` and say it + retired, so the sentence reads as the history it is. + """ + offenders = dangling(source.read_text(encoding="utf-8")) + assert not offenders, ( + f"{source.relative_to(REPO_ROOT)} cites {len(offenders)} path(s) that no " + "file answers to. Re-point at the guard that pins the behaviour now, or " + "name the retired suite without the extension:\n " + + "\n ".join(f"{number}: {citation}" for number, citation in offenders[:8]) + ) From dab2ccf2d3bcc9bf707efb760c7f22de47ad611e Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Sat, 29 Aug 2026 17:50:54 +0100 Subject: [PATCH 2/5] Sweep the citations the Python retirement left pointing at nothing Comments across `rust/` name the test that pins the behaviour they describe, which is most of what makes them worth reading. Retiring the Python implementation (#267) deleted about forty of the files those names referred to, in one commit, and the names stayed. A pointer into nothing is worse than no pointer: it still reads as evidence, so the reader goes looking and cannot tell whether the guard moved, was renamed, or never existed. Sixty sites, in 23 Rust files, one Cargo.toml, one workflow, three surviving Python test files and the divergence table. Every one now names a suite that retired rather than a file to open, in the form the comments that survived the retirement had already settled on. Two were not merely stale but false. The bench workflow told a reader that a named Python spawn-count guard "keeps the gating role" in `ci.yml`; the counts it meant have been argv assertions in the cargo suite since the port. And row 30 of the divergence table still cited an allowance in the parity harness's case list, which retired with the harness. `test/test_citations_resolve.py` is what stops it happening again: a token spelled the way a Python test file is spelled has to resolve to a file, a path as written and a bare name against the test tree. That makes the fix verifiable rather than a tidy nobody can re-check. Closes #308. --- .github/workflows/bench.yml | 7 ++-- AGENTS.md | 12 +++++++ CHANGELOG.md | 23 +++++++++++++ docs/rust-rewrite-plan.md | 2 +- rust/aid/Cargo.toml | 7 ++-- rust/aid/src/rewrite.rs | 6 ++-- rust/aid/tests/rewrite.rs | 10 +++--- rust/devlaunch-core/src/clients/devpod.rs | 7 ++-- rust/devlaunch-core/src/clients/gh.rs | 7 ++-- rust/devlaunch-core/src/domain/config.rs | 3 +- rust/devlaunch-core/src/domain/metadata.rs | 3 +- .../src/domain/workspace_state/tests.rs | 3 +- .../src/flows/branch_manager.rs | 3 +- .../src/flows/completion_cache.rs | 5 +-- rust/devlaunch-core/src/flows/disk_usage.rs | 4 ++- rust/devlaunch-core/src/flows/launch.rs | 4 ++- rust/devlaunch-core/src/flows/lifecycle.rs | 19 ++++++----- rust/devlaunch-core/src/flows/listing.rs | 8 ++--- rust/devlaunch-core/src/flows/migration.rs | 3 +- rust/devlaunch-core/src/flows/provision.rs | 19 +++++------ .../src/flows/provision/lending_contract.rs | 4 +-- rust/devlaunch-core/src/flows/repo_manager.rs | 9 +++--- .../src/flows/workspace_clone.rs | 7 ++-- rust/devlaunch-core/src/timing.rs | 4 ++- rust/devlaunch-test-support/src/cache_tree.rs | 2 +- rust/devlaunch-test-support/src/devpod.rs | 3 +- .../devlaunch-test-support/src/fake_runner.rs | 2 +- rust/devlaunch-test-support/src/lib.rs | 5 +-- rust/dl/src/cli.rs | 5 +-- rust/dl/src/select.rs | 11 ++++--- rust/dl/tests/launch.rs | 2 +- rust/dl/tests/lifecycle.rs | 21 +++++++----- rust/dl/tests/read_side.rs | 2 +- test/fixtures/bench_harness.py | 2 +- test/fixtures/devpod_shim.py | 3 +- test/fixtures/markdown_sections.py | 6 ++-- test/test_bench_doc.py | 2 +- test/test_citations_resolve.py | 32 ++++++++++++------- test/unit/test_devpod_shim.py | 2 +- 39 files changed, 177 insertions(+), 102 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index fb2e81a3..9d67bd77 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -8,8 +8,11 @@ name: Launch benchmark # never fail a build on a slow point. One quiet host's five identical # cold-recreate runs spread 15.2-20.0s; a shared runner is worse, so a hard # wall-clock gate would be either too loose to fire or flaky. Deterministic -# structural guards (test_devpod_spawn_counts.py and its kin) keep the gating -# role, and they live in ci.yml where a gate belongs. +# structural guards keep the gating role: the devpod spawn counts a launch is +# allowed, asserted argv by argv in `flows::launch`'s tests, and their kin +# across the cargo suite. Those run in ci.yml, where a gate belongs. (The +# spawn-count guard was `test_devpod_spawn_counts` in Python until the Python +# tree retired in #267, and the counts came across with it.) # # It is a separate file for exactly that reason: house convention is that a new # job in ci.yml joins `gate`'s `needs` in the same pull request, and `needs` diff --git a/AGENTS.md b/AGENTS.md index 2881f9d5..a015ccfc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -222,6 +222,18 @@ checking rather than trusting, because a private package fails at nothing. See matching on headings. Moving one of those sections between files means moving the path in its guard, in the same change. +- **A citation spelled as a path has to point somewhere.** Comments all over + `rust/` name the test that pins the behaviour they describe, which is what + makes the record worth reading. `test/test_citations_resolve.py` holds every + one of those names to being a file: a path is checked as written, a bare + filename against the test tree. Retiring the Python implementation (#267) + broke about sixty of them at once, which is why the rule exists. When a suite + is gone, name it without the extension and say it retired, the way the + comments now do: `test_purge_ownership` (Python, retired in #267). The + history stays greppable and stops impersonating a live guard. `CHANGELOG.md` + and `docs/rust-port-scope.md` are out of scope as records of what was true + when they were written. + - **No em or en dashes in the README or the `docs/` pages it links.** They read as machine-written. Use a full stop or a comma, or end the sentence, and write a numeric range as "18s to 28s". `test/test_docs_prose.py` asserts it and diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f08151..54fe091e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the dead branch was written straight back, with every fallback that would have answered correctly sitting unreachable behind it. +- **Sixty citations that pointed at nothing now point at something, and a guard + keeps it that way.** Comments across `rust/` name the test that pins the + behaviour they describe, which is most of what makes them worth reading. + Retiring the Python implementation (#267) deleted about forty of the files + those names referred to, in one commit, and the names stayed. A pointer into + nothing is worse than no pointer: it still reads as evidence, so the reader + goes looking and cannot tell whether the guard moved, was renamed, or never + existed. + + Every one of them now names a suite that retired rather than a file to open, + and says so, in the form the surviving comments had already settled on. Two + were not merely stale but false. The bench workflow told a reader that a named + Python spawn-count guard "keeps the gating role" in `ci.yml`, when the counts + it meant have been argv assertions in the cargo suite since the port; and the + divergence table's row 30 still cited an allowance in the parity harness's case + list, which retired with the harness. + + `test/test_citations_resolve.py` is what stops it happening again: a token + spelled the way a Python test file is spelled has to resolve to a file, a path + as written and a bare name against the test tree. `CHANGELOG.md` and + `docs/rust-port-scope.md` are out of scope, being records of what was true when + they were written. + - **A session that dies badly no longer takes your terminal with it.** A terminal is not only a stream: a full screen program switches modes on in the emulator for its own use, the kitty keyboard protocol, bracketed paste, mouse reporting, the diff --git a/docs/rust-rewrite-plan.md b/docs/rust-rewrite-plan.md index 69eb782b..6011bedb 100644 --- a/docs/rust-rewrite-plan.md +++ b/docs/rust-rewrite-plan.md @@ -191,7 +191,7 @@ Nothing diverges silently. | 28 | The `metadata.json` reader takes strict JSON where Python's `json.loads` took its extensions: `NaN`/`Infinity` literals, numbers beyond f64 range (`1e400`, which Python reads as `inf`), and lone-surrogate `\uD800` escapes all read as corruption, so a file carrying one is quarantined to `metadata.json.corrupt` (bytes intact, single slot) and the run starts with an empty cache where Python loaded the records around the value. | serde_json cannot represent a lone surrogate in a Rust `String` and f64 cannot hold `1e400`, so accepting them would mean values the store could not round-trip; no build of dl (Python or Rust) ever writes any of them — only a hand-edited or third-party-written file can carry one — and the quarantine keeps the bytes recoverable. Pinned by `pythons_json_extensions_read_as_corruption_here` in `domain/metadata.rs`. | | 29 | A clone the host refused as not-found gets a second line naming the same repository under an owner the completion cache knows: `Did you mean 'kinisi-robotics/kinisi_ros'? …`. Python printed git's stderr and stopped. The line is added only for a host's own not-found wording (GitHub, GitLab, Bitbucket), only from a clone step, and only when the cache holds the same repo name under another owner *and* not under the owner typed; at most three candidates are named and any remainder is counted. The git text above it is unchanged in every case. | A mistyped owner and a revoked permission produce the same six lines from git, so git's words cannot tell them apart — and devlaunch already holds the answer in the list its own shell completion offers. Additive: nothing is removed or reworded, and a machine with no cache entry sees exactly what it saw before. The suggestion names a *spec*, not a `dl …` command line, because `aid` renders through the same function and its own invocation carries an agent prompt. Pinned by the `wrong_owner_hint` tests in `dl/tests/launch.rs` and `dl/src/render.rs`. | -| 30 | `--stop` and `--rm` are **appendable**: on a line that already said something else they win, and the words they displace are reported rather than refused. `dl 'review this pr' --rm` was `Unknown command 'review this pr'` (exit 1) in both Python and earlier Rust; `dl prune --force --rm` took `prune` as the *workspace* and reported it unknown. A leading verb word is now skipped when finding the target, a word spelling the flag's own verb is absorbed silently, and everything else displaced is named on stderr (`--rm overrode the rest of the line: 'review this pr' was not acted on.`) before anything is removed. `aid` peels the same two flags — plus a `--force` in their company — off the end of its argv before the prompt join, so `aid --rm --force` removes the workspace and hands no agent command to dl; a verb flag before the spec is collected the same way rather than passed through beside a `--` tail dl would refuse. The peel is bounded to the exact words at the very end of the line, so a prompt that mentions `--rm` or ends in a bare `--force` is untouched, and a `--` command tail cannot be overridden at all. | **Superseded by row 32**, which takes both spellings back: `--rm` became docker's `--rm` and `--stop` was retired, so no flag overrides a line any more. A shell makes appending to a recalled line cheap and rewriting its front expensive, so "and now delete it" has to be spellable as a suffix. The notice is what makes it safe rather than a silent discard: the line may now carry an instruction it will not carry out, so a deliberate suffix and a slip have to be distinguishable. Row 15 made the flag spellings work; this makes them the one form that can be typed last. Pinned by the `suffix_verb` tests in `dl/src/cli.rs` and `aid/src/rewrite.rs`, and by the `--aid -- --help` allowance in `rust/tools/parity_cases.txt` — aid's help text is hand-written and was byte-compared against Python's, so documenting a flag Python's aid has not got is where this row becomes visible to the parity harness. | +| 30 | `--stop` and `--rm` are **appendable**: on a line that already said something else they win, and the words they displace are reported rather than refused. `dl 'review this pr' --rm` was `Unknown command 'review this pr'` (exit 1) in both Python and earlier Rust; `dl prune --force --rm` took `prune` as the *workspace* and reported it unknown. A leading verb word is now skipped when finding the target, a word spelling the flag's own verb is absorbed silently, and everything else displaced is named on stderr (`--rm overrode the rest of the line: 'review this pr' was not acted on.`) before anything is removed. `aid` peels the same two flags — plus a `--force` in their company — off the end of its argv before the prompt join, so `aid --rm --force` removes the workspace and hands no agent command to dl; a verb flag before the spec is collected the same way rather than passed through beside a `--` tail dl would refuse. The peel is bounded to the exact words at the very end of the line, so a prompt that mentions `--rm` or ends in a bare `--force` is untouched, and a `--` command tail cannot be overridden at all. | **Superseded by row 32**, which takes both spellings back: `--rm` became docker's `--rm` and `--stop` was retired, so no flag overrides a line any more. A shell makes appending to a recalled line cheap and rewriting its front expensive, so "and now delete it" has to be spellable as a suffix. The notice is what makes it safe rather than a silent discard: the line may now carry an instruction it will not carry out, so a deliberate suffix and a slip have to be distinguishable. Row 15 made the flag spellings work; this makes them the one form that can be typed last. Pinned by the `suffix_verb` tests in `dl/src/cli.rs` and `aid/src/rewrite.rs`. It was also pinned by the `--aid -- --help` allowance in the parity harness's case list — aid's help text is hand-written and was byte-compared against Python's, so documenting a flag Python's aid had not got was where this row became visible to that harness — and the whole harness retired with the Python tree (#267), so the Rust tests are what is left. | | 31 | `prune` is retired as a workspace verb. `dl prune` / `dl prune ` / `dl prune` were Python's second spelling of `rm` and now refuse with `'prune' is no longer a workspace verb. Use 'dl rm' to delete a workspace, or 'dl --prune' to remove the clone directories no workspace opens any more.` (exit 1). The word stays in a `RETIRED` table rather than being dropped, so it is still never read as a workspace name: `dl prune --force --rm` removes `` (row 30's suffix form, with `prune` absorbed as the same verb the flag asks for), and a live verb beside it still wins from either position, so `dl stop prune` stops a workspace named `prune` as before. `dl --prune` is unchanged. Row 15's parenthetical about the `Ignoring --devcontainer` diagnostic naming `rm` for the `prune` spelling is now unreachable. | **Superseded in part by row 32**, which retires row 30's suffix form: `dl prune --rm` is now this row's refusal rather than a removal of ``, and the word is still in `RETIRED` and still never read as a workspace name. One word meant two unrelated commands, separated only by two dashes: `dl prune` deleted a workspace and `dl --prune` deletes clone directories and no workspace. The two failure modes were losing a workspace meant to be kept, and being refused by `--prune takes no workspace` for a reason that sentence cannot explain. `rm` has no such twin, and the retirement is a refusal that names both meanings rather than a silent removal. Pinned by the `prune_is_refused_as_a_verb_from_either_position`, `a_retired_word_is_never_read_as_the_workspace` and `a_recalled_prune_line_is_the_retirement_and_the_pair_is_named_ahead_of_it` tests in `dl/src/cli.rs`. | diff --git a/rust/aid/Cargo.toml b/rust/aid/Cargo.toml index d3df2ff3..942e3ee9 100644 --- a/rust/aid/Cargo.toml +++ b/rust/aid/Cargo.toml @@ -10,9 +10,10 @@ repository.workspace = true # The only devlaunch crate named here, and deliberately: aid is a command-line # rewrite that hands the result to dl, so the whole of devlaunch it can reach is # dl's entry point and the quoting that composes a command line for it. Python -# pins the same rule as a test (`test/unit/test_aid.py` walks aid.py's AST and -# refuses any use of `dl` beyond `main`, `get_version` and `DL_VALUE_OPTIONS`); -# here it is the dependency graph. +# pinned the same rule as a test (`test_aid` walked aid.py's AST and refused any +# use of `dl` beyond `main`, `get_version` and `DL_VALUE_OPTIONS`, and retired +# with the Python tree in #267); here it is the dependency graph, which needs no +# test. dl = { path = "../dl" } # Forwarded rather than declared, so the two binaries cannot report different diff --git a/rust/aid/src/rewrite.rs b/rust/aid/src/rewrite.rs index f05a16a8..00f5710a 100644 --- a/rust/aid/src/rewrite.rs +++ b/rust/aid/src/rewrite.rs @@ -763,9 +763,9 @@ pub(crate) fn build_dl_args(parsed: &AidArgs) -> Option> { #[cfg(test)] mod tests { - //! `test/unit/test_aid.py`'s three parsing classes, which are the whole of aid's - //! own behaviour: what the command line splits into, what shell command comes - //! out, and what dl is handed. + //! The Python `test_aid`'s three parsing classes (retired with the Python tree + //! in #267), which are the whole of aid's own behaviour: what the command line + //! splits into, what shell command comes out, and what dl is handed. use super::*; diff --git a/rust/aid/tests/rewrite.rs b/rust/aid/tests/rewrite.rs index 7b0a1ae3..e4eacc26 100644 --- a/rust/aid/tests/rewrite.rs +++ b/rust/aid/tests/rewrite.rs @@ -385,10 +385,12 @@ fn a_remote_control_variable_that_is_neither_a_yes_nor_a_no_is_refused() { #[test] fn no_prompt_starts_the_agents_plain_session() { - // `test/test_interactive_command.py::TestAidReachesTheTtyTransport`'s first case - // at the boundary: no prompt, so no prompt flags — and the same transport `dl - // ` uses, which is what makes aid a rewrite rather than a launcher. (The - // OpenSSH pty half of that class needs a published ssh alias and is M9's.) + // The Python `test_interactive_command::TestAidReachesTheTtyTransport`'s first + // case at the boundary (that suite retired with the Python tree in #267, and + // this is where the case lives now): no prompt, so no prompt flags — and the + // same transport `dl ` uses, which is what makes aid a rewrite rather than + // a launcher. (The OpenSSH pty half of that class needs a published ssh alias + // and is M9's.) let world = World::with(&["--warm"]); let run = world.aid(&[MAIN]); run.exited(0); diff --git a/rust/devlaunch-core/src/clients/devpod.rs b/rust/devlaunch-core/src/clients/devpod.rs index 421169b9..3963059b 100644 --- a/rust/devlaunch-core/src/clients/devpod.rs +++ b/rust/devlaunch-core/src/clients/devpod.rs @@ -1305,9 +1305,10 @@ mod tests { // ------------------------------------------------------- the timing spans // - // `test/test_timing.py`'s `TestDevpodRoundTripsAreNamed` asserts these labels - // through `main()`, where they arrive from Python's one spawn helper. They - // arrive from this module's three, so this is where they are pinned. + // The Python `test_timing`'s `TestDevpodRoundTripsAreNamed` asserted these + // labels through `main()`, where they arrived from Python's one spawn helper. + // They arrive from this module's three, so this is where they are pinned now; + // that suite retired with the Python tree (#267). /// The span labels `record` produced, in order. /// diff --git a/rust/devlaunch-core/src/clients/gh.rs b/rust/devlaunch-core/src/clients/gh.rs index 9d49d625..f3e05f2f 100644 --- a/rust/devlaunch-core/src/clients/gh.rs +++ b/rust/devlaunch-core/src/clients/gh.rs @@ -589,9 +589,10 @@ mod tests { // -------------------------------------------------------- the timing span // - // `test/test_timing.py`'s `TestTransportAndGitGhCallsAreNamed` and - // `TestHostPrepIsAStage` assert this from outside; Python's span and stage sit - // inside `_token_from_gh_cli`, so this is the module that owns them. + // The Python `test_timing`'s `TestTransportAndGitGhCallsAreNamed` and + // `TestHostPrepIsAStage` asserted this from outside, and retired with the + // Python tree (#267); Python's span and stage sat inside `_token_from_gh_cli`, + // so this is the module that owns them. /// The stages and spans a measured `record` produced. fn measured(record: impl FnOnce()) -> timing::Document { diff --git a/rust/devlaunch-core/src/domain/config.rs b/rust/devlaunch-core/src/domain/config.rs index 55526796..39a40559 100644 --- a/rust/devlaunch-core/src/domain/config.rs +++ b/rust/devlaunch-core/src/domain/config.rs @@ -243,7 +243,8 @@ struct StoredCleanup { #[cfg(test)] mod tests { - //! `test/test_worktree_config.py`, re-pinned. + //! The Python `test_worktree_config`, re-pinned here when it retired with the + //! Python tree (#267). //! //! Python's tests construct `WorktreeConfig(...)` directly and check //! `to_dict`; both are gone here, because nothing in devlaunch writes this diff --git a/rust/devlaunch-core/src/domain/metadata.rs b/rust/devlaunch-core/src/domain/metadata.rs index fd3d7dff..076d268c 100644 --- a/rust/devlaunch-core/src/domain/metadata.rs +++ b/rust/devlaunch-core/src/domain/metadata.rs @@ -1190,7 +1190,8 @@ impl serde_json::ser::Formatter for PythonJsonFormatter<'_> { #[cfg(test)] mod tests { - //! `test/test_worktree_storage.py`, re-pinned. + //! The Python `test_worktree_storage`, re-pinned here when it retired with the + //! Python tree (#267). //! //! Python patched two seams to observe behaviour it could not otherwise see //! — `Path.replace` to watch the atomic rename, `storage.save` to count diff --git a/rust/devlaunch-core/src/domain/workspace_state/tests.rs b/rust/devlaunch-core/src/domain/workspace_state/tests.rs index defde811..a8295152 100644 --- a/rust/devlaunch-core/src/domain/workspace_state/tests.rs +++ b/rust/devlaunch-core/src/domain/workspace_state/tests.rs @@ -8,7 +8,8 @@ //! walking up into an ancestor repository) were both invisible to everything //! except a real git. //! -//! Ported from `test/test_workspace_state.py`'s five module-level classes: +//! Ported from the Python `test_workspace_state` (retired with the Python tree +//! in #267), whose five module-level classes were: //! `TestWhatACloneHolds`, `TestWhenGitCannotBeAsked`, //! `TestGitIsPinnedToItsWorkTreeToo`, `TestADirectoryThatCannotBeLookedAt`, //! `TestTheAnswersAreTotal` and `TestNamingWhatIsUnsaved`. Its remaining classes diff --git a/rust/devlaunch-core/src/flows/branch_manager.rs b/rust/devlaunch-core/src/flows/branch_manager.rs index 9f254f06..3c78e2ae 100644 --- a/rust/devlaunch-core/src/flows/branch_manager.rs +++ b/rust/devlaunch-core/src/flows/branch_manager.rs @@ -333,7 +333,8 @@ impl<'r> BranchManager<'r> { #[cfg(test)] mod tests { - //! `test/test_worktree_branch_manager.py`, re-pinned at the argv seam. + //! The Python `test_worktree_branch_manager`, re-pinned at the argv seam when + //! it retired with the Python tree (#267). //! //! Python's `TestEnsureBranchExists` classes patch the four verbs on the class //! and assert which of them were called with what. There is nothing to patch diff --git a/rust/devlaunch-core/src/flows/completion_cache.rs b/rust/devlaunch-core/src/flows/completion_cache.rs index a442a201..e3f75bda 100644 --- a/rust/devlaunch-core/src/flows/completion_cache.rs +++ b/rust/devlaunch-core/src/flows/completion_cache.rs @@ -382,10 +382,11 @@ pub(crate) fn local_branches(git: &Git<'_>, repos_dir: &Path, owner_repo: &str) #[cfg(test)] mod tests { - //! Ported from `test/test_dl.py`'s `TestCacheFunctions`, + //! Ported from the Python `test_dl`'s `TestCacheFunctions`, //! `TestCompletionCacheFreshness`, `TestNoRefreshForCacheFreeCommands` and //! `TestCacheReadingCommandsWarmTheCache`, plus - //! `test/unit/test_workspace_listing.py::TestCompletionsAreBuiltFromWhateverCanBeRead`. + //! `test_workspace_listing::TestCompletionsAreBuiltFromWhateverCanBeRead`. + //! Both suites retired with the Python tree (#267). //! //! The two files' bytes are goldens: they were read off a scratch cache written //! by the Python build, run against the devpod shim. diff --git a/rust/devlaunch-core/src/flows/disk_usage.rs b/rust/devlaunch-core/src/flows/disk_usage.rs index df83845e..e5033a54 100644 --- a/rust/devlaunch-core/src/flows/disk_usage.rs +++ b/rust/devlaunch-core/src/flows/disk_usage.rs @@ -468,7 +468,9 @@ impl UsageWire { #[cfg(test)] mod tests { - //! What `test/unit/test_disk_usage.py` pins, on this side of the port. + //! What the Python `test_disk_usage` pinned, on this side of the port. That + //! suite retired with the Python tree (#267); this is where its subject lives + //! now. //! //! The number under test is *exclusive* bytes: what deleting this directory //! would free, counting a file only when every one of its links lies inside diff --git a/rust/devlaunch-core/src/flows/launch.rs b/rust/devlaunch-core/src/flows/launch.rs index 6b9eda04..9d7f049c 100644 --- a/rust/devlaunch-core/src/flows/launch.rs +++ b/rust/devlaunch-core/src/flows/launch.rs @@ -3556,7 +3556,9 @@ mod tests { } /// Every devpod invocation, without the leading `devpod`, in order — the - /// shape `test_devpod_spawn_counts.py` asserts. + /// shape the Python `test_devpod_spawn_counts` asserted before it retired + /// with the Python tree (#267). The spawn counts it gated on are gated + /// here now, by the assertions in this module that read this list. fn devpod_commands(&self) -> Vec> { self.runner.args_to("devpod") } diff --git a/rust/devlaunch-core/src/flows/lifecycle.rs b/rust/devlaunch-core/src/flows/lifecycle.rs index b9eb9ba2..b7b6f1b5 100644 --- a/rust/devlaunch-core/src/flows/lifecycle.rs +++ b/rust/devlaunch-core/src/flows/lifecycle.rs @@ -3029,18 +3029,17 @@ mod tests { //! is ordinary on bind and overlay mounts. Where the filesystem does not deny, //! the test steps aside instead of asserting something it cannot reproduce. //! - //! Ported from `test/unit/test_purge_partial_removal.py`, - //! `test/unit/test_purge_ownership.py`'s purge-action classes, - //! `test/unit/test_workspace_listing.py::TestPurgeWillNotActOnAListItCouldNotRead`, - //! `test/test_workspace_state.py`'s `TestTheDeleteGuard` and - //! `TestForcedRemoveIsEnsureAbsent`, `test/test_dl.py`'s + //! Ported from these Python suites, all of which retired with the Python tree + //! (#267) — the names are what to grep the history for, not files to open: + //! `test_purge_partial_removal`, `test_purge_ownership`'s purge-action + //! classes, `test_workspace_listing::TestPurgeWillNotActOnAListItCouldNotRead`, + //! `test_workspace_state`'s `TestTheDeleteGuard` and + //! `TestForcedRemoveIsEnsureAbsent`, `test_dl`'s //! `TestBackgroundRefreshSpawning`, `TestRefreshChildRechecksFreshness` and //! `TestWorkspaceCommandsRefreshOnceAfterwards`, - //! `test/unit/test_updater_fetch_sweep.py`, - //! `test/unit/test_stored_workspace_id.py`, - //! `test/unit/test_prune_orphaned_clones.py`, - //! `test/unit/test_workspace_source_placement.py` and - //! `test/unit/test_reconcile_orphaned_workspaces.py`. + //! `test_updater_fetch_sweep`, `test_stored_workspace_id`, + //! `test_prune_orphaned_clones`, `test_workspace_source_placement` and + //! `test_reconcile_orphaned_workspaces`. //! //! `remove_tree_as_far_as_it_goes` lives in `flows::repo_manager` beside the //! cleanup it is the counterpart of, and is tested from here: `dl --purge` is diff --git a/rust/devlaunch-core/src/flows/listing.rs b/rust/devlaunch-core/src/flows/listing.rs index 6b76c9a6..4126f6b3 100644 --- a/rust/devlaunch-core/src/flows/listing.rs +++ b/rust/devlaunch-core/src/flows/listing.rs @@ -1220,10 +1220,10 @@ pub(crate) use crate::json::as_python_writes_it as json_as_python_writes_it; #[cfg(test)] mod tests { - //! Ported from `test/unit/test_workspace_listing.py`, - //! `test/unit/test_workspace_source.py`'s discovery and description classes, - //! `test/unit/test_purge_ownership.py`'s classification classes, - //! `test/test_workspace_state.py::TestTheJsonListing`, and `test/test_dl.py`'s + //! Ported from these Python suites, all retired with the Python tree (#267): + //! `test_workspace_listing`, `test_workspace_source`'s discovery and + //! description classes, `test_purge_ownership`'s classification classes, + //! `test_workspace_state::TestTheJsonListing`, and `test_dl`'s //! `TestListWorkspaces`, `TestGetWorkspaceIds`, `TestParseOwnerRepoFromUrl`, //! `TestDiscoverReposFromWorkspaces`, `TestDiscoverReposFromCacheDir`, //! `TestGetKnownRepos` and `TestPrintFunctions`. diff --git a/rust/devlaunch-core/src/flows/migration.rs b/rust/devlaunch-core/src/flows/migration.rs index dadc2faa..2d4f332a 100644 --- a/rust/devlaunch-core/src/flows/migration.rs +++ b/rust/devlaunch-core/src/flows/migration.rs @@ -431,7 +431,8 @@ fn write_lines(path: &Path, lines: &[String]) -> Listing { #[cfg(test)] mod tests { - //! `test/test_worktree_migration.py`, re-pinned. + //! The Python `test_worktree_migration`, re-pinned here when it retired with + //! the Python tree (#267). //! //! Every test builds its own cache under a temp directory. Nothing here may //! read or write the real cache: the migration renames directories that can diff --git a/rust/devlaunch-core/src/flows/provision.rs b/rust/devlaunch-core/src/flows/provision.rs index b09bf7de..ac1c767a 100644 --- a/rust/devlaunch-core/src/flows/provision.rs +++ b/rust/devlaunch-core/src/flows/provision.rs @@ -2499,19 +2499,20 @@ mod tests { //! Three kinds of test, because the module is three things: //! //! - **Goldens.** Every script and fragment is compared against the exact - //! string `devlaunch/tools.py` renders, generated by importing the module - //! (`pixi run python -c 'from devlaunch import tools; print(tools.probe_script())'`). - //! A shell fix has to move both sides, which is the point: these scripts run - //! in someone else's container and the bytes are the shipped behaviour. + //! string `devlaunch/tools.py` rendered, transcribed from that module before + //! it retired with the Python tree (#267); see the note on the `PYTHON_` + //! prefix below for why they keep its name. These scripts run in someone else's + //! container and the bytes are the shipped behaviour, so a shell fix moves + //! the golden with the renderer. //! - **Real runs.** The probe, the transfer, the setup pass and the zellij //! stage are executed by a real bash against scratch `$HOME`s on a stripped //! PATH — the only way to find out what a script *does*, and the only way the //! convergence claim (a lend the next probe recognises) can be tested at all. - //! - **The flow, over a recording runner.** The same fake shape - //! `test/unit/test_tools.py` uses: one exit status per trip, the last - //! repeating, plus what each trip put on stdin. + //! - **The flow, over a recording runner.** The same fake shape the Python + //! `test_tools` used: one exit status per trip, the last repeating, plus + //! what each trip put on stdin. //! - //! `test/unit/test_tools.py` is re-pinned here in full, except + //! `test_tools` (Python, retired in #267) is re-pinned here in full, except //! `TestWorkspaceUpInstallsTools`, which is about `dl.py`'s wiring (that `up` //! and `dl up` call this at all) and belongs to the launch flow. @@ -2788,7 +2789,7 @@ fi "#; // ======================================================================= - // the fake runner: test_tools.py's Runner, as a Runner impl + // the fake runner: the Python test_tools's Runner, as a Runner impl // ======================================================================= /// What one scripted trip answers. diff --git a/rust/devlaunch-core/src/flows/provision/lending_contract.rs b/rust/devlaunch-core/src/flows/provision/lending_contract.rs index 6e4d8917..5ed2913f 100644 --- a/rust/devlaunch-core/src/flows/provision/lending_contract.rs +++ b/rust/devlaunch-core/src/flows/provision/lending_contract.rs @@ -37,8 +37,8 @@ //! lend may not advertise a verification of the payload that the lend does not //! perform. //! -//! These tests were `test/test_lending_doc.py` until the Python implementation was -//! retired (#267). They are here rather than there because every constant and every +//! These tests were the Python `test_lending_doc` until the Python implementation +//! was retired (#267). They are here rather than there because every constant and every //! generator they read is in this module: from Python they would have had to be //! *parsed* out of this file, and a prose-drift guard that can only read the source //! it is guarding is one rename away from silently passing. diff --git a/rust/devlaunch-core/src/flows/repo_manager.rs b/rust/devlaunch-core/src/flows/repo_manager.rs index 942f7280..d2fee84a 100644 --- a/rust/devlaunch-core/src/flows/repo_manager.rs +++ b/rust/devlaunch-core/src/flows/repo_manager.rs @@ -1791,11 +1791,10 @@ pub(crate) mod tests { //! repository in the same temp directory — a real clone, no network — and are //! marked in their names. //! - //! Ported from `test/test_worktree_repo_manager.py`, - //! `test/integration/test_repo_manager_real.py`, - //! `test/integration/test_repo_manager_recovery.py`, - //! `test/integration/test_clone_race.py` and the fetch-count half of - //! `test/test_cold_launch_fetches.py`. + //! Ported from these Python suites, all retired with the Python tree (#267): + //! `test_worktree_repo_manager`, the integration suites + //! `test_repo_manager_real`, `test_repo_manager_recovery` and + //! `test_clone_race`, and the fetch-count half of `test_cold_launch_fetches`. use std::process::Command; use std::sync::mpsc; diff --git a/rust/devlaunch-core/src/flows/workspace_clone.rs b/rust/devlaunch-core/src/flows/workspace_clone.rs index 9dcdee2c..d0c287ab 100644 --- a/rust/devlaunch-core/src/flows/workspace_clone.rs +++ b/rust/devlaunch-core/src/flows/workspace_clone.rs @@ -1340,9 +1340,10 @@ fn say_branch( #[cfg(test)] mod tests { - //! `test/test_workspace_clone.py`, `test/test_cold_launch_fetches.py`, the - //! token half of `test/test_repo_lock_cycles.py`, and the four integration - //! files about what a clone shares with the cache. + //! The Python `test_workspace_clone`, `test_cold_launch_fetches`, the token + //! half of `test_repo_lock_cycles`, and the four integration files about what + //! a clone shares with the cache — all retired with the Python tree (#267), + //! and re-pinned here. //! //! **The argv seam does more here than Python's mocks did.** Python replaced //! `RepositoryManager` and `BranchManager` with `MagicMock`s, so a test of diff --git a/rust/devlaunch-core/src/timing.rs b/rust/devlaunch-core/src/timing.rs index 446682b2..5feb6e35 100644 --- a/rust/devlaunch-core/src/timing.rs +++ b/rust/devlaunch-core/src/timing.rs @@ -947,7 +947,9 @@ impl Drop for Exclusive { #[cfg(test)] mod tests { - //! What `test/test_timing.py` pins, on this side of the port. + //! What the Python `test_timing` pinned, on this side of the port. It retired + //! with the Python tree (#267), except for the bench-harness half, which moved + //! to `test/test_bench_doc.py` and `test/fixtures/bench_harness.py`. //! //! Its `TestBenchHarness`, `TestTheDocumentedColdReset`, //! `TestBenchRecordsEachRunsStages` and `TestEveryDocumentedInvocation` diff --git a/rust/devlaunch-test-support/src/cache_tree.rs b/rust/devlaunch-test-support/src/cache_tree.rs index a3c70938..4a757d71 100644 --- a/rust/devlaunch-test-support/src/cache_tree.rs +++ b/rust/devlaunch-test-support/src/cache_tree.rs @@ -23,7 +23,7 @@ //! a listing in the test. That one *is* read off today's binary, deliberately and //! unlike every other golden in these suites — there is no Python left to capture //! it from. What it catches is a **change**, which is what the parity cases caught -//! too (`parity_cases.txt`: "the regression tripwire"). Every line of it should be +//! too, under the heading they gave it: "the regression tripwire". Every line of it should be //! explainable at review; a line nobody can explain is the finding. //! //! # What is left out, and why diff --git a/rust/devlaunch-test-support/src/devpod.rs b/rust/devlaunch-test-support/src/devpod.rs index dc4e53c2..fb0074c8 100644 --- a/rust/devlaunch-test-support/src/devpod.rs +++ b/rust/devlaunch-test-support/src/devpod.rs @@ -1,6 +1,7 @@ //! The fake devpod behind the fake runner: a workspace state machine. //! -//! `test/fixtures/devpod_mock.py`'s design, re-homed behind the typed +//! The Python `devpod_mock` fixture's design (retired with the Python tree in +//! #267), re-homed behind the typed //! [`Runner`](devlaunch_runner::Runner) seam and brought up to the //! fidelity `test/fixtures/devpod_shim.py` proved out. What matters is that the //! shapes are real devpod's shapes, because the code under test parses them: diff --git a/rust/devlaunch-test-support/src/fake_runner.rs b/rust/devlaunch-test-support/src/fake_runner.rs index 4bf333ab..5ea6973b 100644 --- a/rust/devlaunch-test-support/src/fake_runner.rs +++ b/rust/devlaunch-test-support/src/fake_runner.rs @@ -88,7 +88,7 @@ impl Scripted { /// What to do about a spawn nothing scripted and no state machine owns. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum Unscripted { - /// Exit 0 with no output, as `devpod_mock.py` does. + /// Exit 0 with no output, as the Python `devpod_mock` fixture did. #[default] Succeed, /// Fail the test, naming the argv. For a test that means to account for diff --git a/rust/devlaunch-test-support/src/lib.rs b/rust/devlaunch-test-support/src/lib.rs index 0c2204a2..d07940d8 100644 --- a/rust/devlaunch-test-support/src/lib.rs +++ b/rust/devlaunch-test-support/src/lib.rs @@ -1,6 +1,7 @@ //! Dev-only test support (#251 §2): the single fake `Runner` (call recorder + -//! argv->response table + devpod workspace state machine — devpod_mock.py's -//! design re-homed), git fixtures, and scenario builders. A separate crate so +//! argv->response table + devpod workspace state machine — the design of the +//! Python `devpod_mock` fixture, re-homed before that tree retired in #267), +//! git fixtures, and scenario builders. A separate crate so //! nothing test-only can leak into shipped builds; importable by core's //! tests, dl's tests, and wf's contract tests. //! diff --git a/rust/dl/src/cli.rs b/rust/dl/src/cli.rs index 0ca76d8f..76803b19 100644 --- a/rust/dl/src/cli.rs +++ b/rust/dl/src/cli.rs @@ -1087,8 +1087,9 @@ pub(crate) fn argv_without_devcontainer(argv: &[String]) -> Vec<&str> { mod tests { //! The grammar decisions, as tables. //! - //! What `test/test_dl.py`'s dispatch expectations and the module docstring's - //! usage block say, re-expressed against [`resolve`] — plus the three + //! What the Python `test_dl`'s dispatch expectations and the module + //! docstring's usage block said, re-expressed against [`resolve`] when both + //! retired with the Python tree (#267) — plus the three //! divergence rows the grammar carries, each with the input that distinguishes //! it from Python. diff --git a/rust/dl/src/select.rs b/rust/dl/src/select.rs index 3b612be3..40f198f7 100644 --- a/rust/dl/src/select.rs +++ b/rust/dl/src/select.rs @@ -8,8 +8,10 @@ //! //! What it offers is Python's list and Python's order, with one row per workspace //! devpod lists — no filtering of any kind, so a workspace whose source devlaunch -//! cannot read is *offered* rather than quietly dropped -//! (`test/unit/test_workspace_source.py::TestTheFuzzyPickerOffersEverySource`). +//! cannot read is *offered* rather than quietly dropped; that is pinned by this +//! module's own tests, which took it over from the Python +//! `test_workspace_source::TestTheFuzzyPickerOffersEverySource` when the Python +//! tree retired (#267). //! //! **The columns are not Python's.** `dl.py::fuzzy_select_workspace` drew //! `{id} | {kind} | {detail}`, where the last two came from `describe_source`; this @@ -829,8 +831,9 @@ impl SkimItem for Row { #[cfg(test)] mod tests { - //! `test/unit/test_workspace_source.py::TestTheFuzzyPickerOffersEverySource`, - //! which is the spec for what the picker offers: the labels, and that picking + //! The Python `test_workspace_source::TestTheFuzzyPickerOffersEverySource` + //! (retired with the Python tree in #267), which was and is the spec for what + //! the picker offers: the labels, and that picking //! one maps back to a workspace. Both are the pure half of this module, and the //! workspaces are built the way a real run gets them — from a `devpod list` //! answer through the listing parser — so the label is pinned over the whole diff --git a/rust/dl/tests/launch.rs b/rust/dl/tests/launch.rs index d320e8f3..1d2c5eae 100644 --- a/rust/dl/tests/launch.rs +++ b/rust/dl/tests/launch.rs @@ -1615,7 +1615,7 @@ fn a_refused_launch_asks_for_no_refresh_because_nothing_changed() { #[test] fn the_prose_timing_summary_names_a_launchs_round_trips() { - // `test_timing.py`'s prose mode at the boundary. The labels are Python's + // The Python `test_timing`'s prose mode at the boundary. The labels are Python's // `" ".join(cmd[:2])` for every devpod call, and the vocabulary is read from // outside this repo (a trend job that decomposes a launch), so a name here is // renamed only deliberately. diff --git a/rust/dl/tests/lifecycle.rs b/rust/dl/tests/lifecycle.rs index c5ea86bb..ddb01b98 100644 --- a/rust/dl/tests/lifecycle.rs +++ b/rust/dl/tests/lifecycle.rs @@ -8,6 +8,11 @@ //! not come from Python — the `--stop`/`--rm` flag spellings, which Python parsed //! as a workspace name — the divergence row is cited beside it. //! +//! The case comments below name the Python suite each case came from. Every one of +//! those retired with the Python tree (#267), so they are provenance to grep the +//! history for rather than files to open; what pins the behaviour now is the case +//! the name sits on. +//! //! Python's diagnostics go to stderr as the bare message: `dl.py` configures //! `logging.basicConfig(level=logging.INFO, format="%(message)s")`, so `info`, //! `warning` and `error` all arrive with no level, no logger name and no prefix. @@ -569,7 +574,7 @@ fn a_stop_addresses_the_workspace_and_says_nothing() { #[test] fn a_stop_reaches_the_workspace_the_record_names() { - // devlaunch#88, and `test_stored_workspace_id.py`'s + // devlaunch#88, and the Python `test_stored_workspace_id`'s // TestTheSubcommandsAddressWhatWasResolved at the boundary: the record holds a // devpod workspace id this build does not derive, devpod has only that one, and // the stop has to reach it. The derived id is in the line because the two are @@ -843,7 +848,7 @@ fn a_clone_git_cannot_be_asked_about_is_refused_for_not_knowing() { #[test] fn a_clone_holding_a_commit_no_remote_has_is_refused_and_named_as_that() { - // `test_workspace_state.py`'s guard texts: uncommitted work and unpushed commits + // The Python `test_workspace_state`'s guard texts: uncommitted work and unpushed commits // are different losses, and the refusal says which — a user who reads // "uncommitted" for a committed change would go looking in the wrong place. let world = World::with(&["--unpushed"]); @@ -1459,7 +1464,7 @@ fn an_rme_devpod_would_not_finish_leaves_the_shell_up_too() { #[test] fn a_command_that_changed_the_workspace_list_refreshes_the_completions_behind_it() { - // `test_dl.py`'s refresh classes, spawn half: the child is this build, run as + // The Python `test_dl`'s refresh classes, spawn half: the child is this build, run as // ` --update-cache --force`, detached — so what can be observed from // out here is that a completion cache appears without this process waiting for // one, and that the child asked devpod for the list. @@ -1528,7 +1533,7 @@ fn the_refresh_child_writes_the_cache_and_sweeps_the_bare_clones() { #[test] fn the_child_migrates_the_cache_like_every_other_run() { - // `test_updater_fetch_sweep.py`'s TestTheChildMigratesLikeEveryOtherRun: a + // The Python `test_updater_fetch_sweep`'s TestTheChildMigratesLikeEveryOtherRun: a // detached child is the worst place to skip the one-shot id-scheme migration, // because nobody is watching it write records in a shape the rest of dl no // longer reads. It reaches metadata through the same construction point every @@ -1686,7 +1691,7 @@ fn a_purge_deletes_the_workspaces_devlaunch_made_and_its_cache() { #[test] fn a_purge_that_could_not_remove_everything_says_which_paths_refused() { - // `test_purge_partial_removal.py`'s TestTheReportIsActionable and + // The Python `test_purge_partial_removal`'s TestTheReportIsActionable and // TestThePurgeSaysWhichOfTheThreeHappened, at the boundary: the headline says // *which* of the three endings this was, the paths and their reasons are what // somebody acts on, and the `sudo rm -rf` line is quoted. @@ -1779,7 +1784,7 @@ fn a_purge_that_deleted_workspaces_and_found_no_cache_says_nothing_about_the_cac #[test] fn a_purge_that_cannot_read_the_workspace_list_refuses_rather_than_purging_nothing() { - // `test_workspace_listing.py`'s purge-will-not-act half: a purge that quietly + // The Python `test_workspace_listing`'s purge-will-not-act half: a purge that quietly // did nothing used to look exactly like a purge that had nothing to do. let world = World::base(); world.devpod_answers(&["list"], 1, "context not found: default\n"); @@ -1840,7 +1845,7 @@ Dropping 1 record(s) of directories already gone. #[test] fn a_prune_answered_no_removes_nothing_and_the_report_is_the_read_only_view() { - // `test_prune_orphaned_clones.py`'s report and input classes, and + // The Python `test_prune_orphaned_clones`'s report and input classes, and // TestTheDiskThisCommandDoesNotFree: the plan names what is going, what is // staying and why, and the run ends on the boundary sentence whichever way it // ends. @@ -2055,7 +2060,7 @@ Nothing here is deleted. `dl rm` is how one goes, if it should. #[test] fn a_reconcile_answered_no_changes_nothing() { - // `test_reconcile_orphaned_workspaces.py`'s report and confirm classes: the + // The Python `test_reconcile_orphaned_workspaces`'s report and confirm classes: the // repair the migration's notice promises is stated before it is consented to, // and nothing here is deleted. let world = World::with(&["--orphan"]); diff --git a/rust/dl/tests/read_side.rs b/rust/dl/tests/read_side.rs index 11f631fb..6f5a1a5a 100644 --- a/rust/dl/tests/read_side.rs +++ b/rust/dl/tests/read_side.rs @@ -734,7 +734,7 @@ fn install_edits_the_rc_file_it_is_given() { #[test] fn the_json_listing_migrates_the_cache_and_the_table_does_not() { - // `test_worktree_migration.py`'s TestWiring, at the boundary: the migration + // The Python `test_worktree_migration`'s TestWiring, at the boundary: the migration // runs from the one place the clone manager is built, so the commands that // never build one never migrate. `--ls` reads devpod and nothing else; // `--ls --json` reads the records, so it is the one that migrates. diff --git a/test/fixtures/bench_harness.py b/test/fixtures/bench_harness.py index 919970a8..b768e974 100644 --- a/test/fixtures/bench_harness.py +++ b/test/fixtures/bench_harness.py @@ -6,7 +6,7 @@ hermetic, and able to describe launches that would be awkward to arrange for real (a run that reported its stages and then failed). -These helpers lived in `test/test_timing.py`, which judged the Python +These helpers lived in the Python `test_timing`, which judged that implementation's timing module and went with it (#267). The bench scripts are language-agnostic tooling and stayed, so the harness that drives them stayed too -- with the timing vocabulary now read from the Rust source that owns it rather diff --git a/test/fixtures/devpod_shim.py b/test/fixtures/devpod_shim.py index bdc86635..b9f718c0 100755 --- a/test/fixtures/devpod_shim.py +++ b/test/fixtures/devpod_shim.py @@ -4,7 +4,8 @@ Stands on PATH under the name `devpod`, in front of *any* dl implementation — it must know nothing about which one is calling, so it is a standalone, stdlib-only program: no devlaunch imports, no pytest, no test helpers. It -re-homes `test/fixtures/devpod_mock.py`'s proven design as a separate process: +re-homes the proven design of the Python `devpod_mock` fixture (retired with +that tree in #267) as a separate process: - **workspace state machine**, persisted to the JSON file named by ``DEVPOD_SHIM_STATE`` so it survives across the many short-lived processes a diff --git a/test/fixtures/markdown_sections.py b/test/fixtures/markdown_sections.py index 3d197f83..fdb83d8f 100644 --- a/test/fixtures/markdown_sections.py +++ b/test/fixtures/markdown_sections.py @@ -2,9 +2,9 @@ Shared because three files aim assertions at README sections and each of them needs the same thing: the text under one heading and nothing near it. It used to -live in `test_lending_doc.py` and be imported from there, which made two unrelated -test files depend on a third for a helper that is about markdown rather than about -lending. +live in the Python `test_lending_doc` (retired with the Python tree in #267) and be +imported from there, which made two unrelated test files depend on a third for a +helper that is about markdown rather than about lending. """ from __future__ import annotations diff --git a/test/test_bench_doc.py b/test/test_bench_doc.py index 76d0b410..a91e4609 100644 --- a/test/test_bench_doc.py +++ b/test/test_bench_doc.py @@ -348,7 +348,7 @@ def test_the_epilogs_cold_reset_is_a_command_dl_actually_accepts(devpod_shim): run against a devpod that has nothing to delete, the state every cold bench's first reset meets. - Lived in `test_timing.py` until the Python implementation was retired (#267), + Lived in the Python `test_timing` until that implementation was retired (#267), and moved here rather than being dropped: this file already guards the document's copies of the same commands, and while the document currently points at this epilog instead of publishing a reset of its own, the epilog's is the only one diff --git a/test/test_citations_resolve.py b/test/test_citations_resolve.py index 928ff480..ee5c0d17 100644 --- a/test/test_citations_resolve.py +++ b/test/test_citations_resolve.py @@ -42,6 +42,7 @@ from __future__ import annotations +import os import re from pathlib import Path @@ -52,9 +53,13 @@ # Records rather than pointers: what they say was true when it was written. ARCHIVAL = {"CHANGELOG.md", "docs/rust-port-scope.md"} -# The trees a citation can be written in. Everything else (`rust/target`, a +# The trees a citation can be written in. Everything else (a pixi environment, a # vendored dependency, whatever a tool drops) is not prose this repository owns. SOURCE_TREES = (".github", "docs", "rust", "scripts", "test") +# Cargo's build output lives inside one of them and is nobody's prose. It is also +# there or not there depending on whether anything has been built, which would +# make the set of cases this test collects depend on the state of the machine. +BUILD_OUTPUT = "rust/target" SOURCE_SUFFIXES = {".md", ".py", ".rs", ".sh", ".toml", ".yaml", ".yml"} TOP_LEVEL_DOCS = ("AGENTS.md", "CLAUDE.md", "README.md") @@ -83,22 +88,25 @@ def sources() -> list[Path]: found.append(page) found.extend(page for page in REPO_ROOT.glob("*.toml") if page.is_file()) for tree in SOURCE_TREES: - for path in sorted((REPO_ROOT / tree).rglob("*")): - if path.is_file() and path.suffix in SOURCE_SUFFIXES: - found.append(path) - return [ - path - for path in found - if path.relative_to(REPO_ROOT).as_posix() not in ARCHIVAL - ] + for directory, subdirectories, files in os.walk(REPO_ROOT / tree): + here = Path(directory) + # Pruned rather than filtered afterwards: cargo's output holds tens of + # thousands of files and walking it is most of this test's runtime. + subdirectories[:] = sorted( + name + for name in subdirectories + if not (here / name).is_relative_to(REPO_ROOT / BUILD_OUTPUT) + ) + found.extend( + here / name for name in sorted(files) if Path(name).suffix in SOURCE_SUFFIXES + ) + return [path for path in found if path.relative_to(REPO_ROOT).as_posix() not in ARCHIVAL] SOURCES = sources() # Every Python test file that exists, by basename, for the bare-name rule. -TEST_FILES = { - path.name for path in (REPO_ROOT / "test").rglob("test_*.py") if path.is_file() -} +TEST_FILES = {path.name for path in (REPO_ROOT / "test").rglob("test_*.py") if path.is_file()} def _resolves(citation: str) -> bool: diff --git a/test/unit/test_devpod_shim.py b/test/unit/test_devpod_shim.py index b195b7f2..8aa0cd53 100644 --- a/test/unit/test_devpod_shim.py +++ b/test/unit/test_devpod_shim.py @@ -1,6 +1,6 @@ """The fake `devpod` PATH shim (#252 §3), tested as the program it is. -The shim re-homes `test/fixtures/devpod_mock.py`'s design — call recorder, +The shim re-homes the Python `devpod_mock` fixture's design — call recorder, argv→response table, workspace state machine — as a standalone executable, so it can stand on PATH in front of *any* dl implementation. Every test here spawns it as a separate process, because that is the only way it is ever used. From 25d4f86422618b0011af87d4080e7e3c60913754 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Sat, 29 Aug 2026 18:47:01 +0100 Subject: [PATCH 3/5] Two pointers in the sweep were themselves pointing at nothing Review found both, and both are the defect this ticket is about. Row 30 of the divergence table still said its behaviour was "pinned by the `suffix_verb` tests"; `grep -rn suffix_verb rust/` returns nothing. Those tests were deleted with the behaviour itself, in the change row 32 records -- the same commit that took `Overridden` and `pick_target`, which row 32 already says are gone. So the row has no pin at all now, which is what being superseded means, and it says that instead of naming tests nobody can open. Row 32's own pins were checked and all resolve. The Cargo.toml comment I added claimed the dependency graph enforces "the same rule" a retired Python AST test did, which limited aid to three names of dl. It does not: aid's source reaches dl's signal handlers, terminal helpers, version markers and rendering, and two of those (`shell`, `env_str`) are devlaunch-core's own arriving through a dl re-export. What the line does enforce is worth stating on its own -- aid cannot name devlaunch-core or devlaunch-runner, so a new reach into either has to be a `pub use` added to dl and reviewed there -- so it now says that, and says the Python rule was tighter rather than the same. --- CHANGELOG.md | 6 ++++-- docs/rust-rewrite-plan.md | 2 +- rust/aid/Cargo.toml | 19 +++++++++++++------ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54fe091e..3c83d115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,8 +39,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 were not merely stale but false. The bench workflow told a reader that a named Python spawn-count guard "keeps the gating role" in `ci.yml`, when the counts it meant have been argv assertions in the cargo suite since the port; and the - divergence table's row 30 still cited an allowance in the parity harness's case - list, which retired with the harness. + divergence table's row 30 claimed two pins it no longer has, an allowance in + the parity harness's case list and a set of `suffix_verb` tests, the first + retired with the harness and the second deleted with the behaviour itself. That + row is superseded, so nothing pins it, and it now says so. `test/test_citations_resolve.py` is what stops it happening again: a token spelled the way a Python test file is spelled has to resolve to a file, a path diff --git a/docs/rust-rewrite-plan.md b/docs/rust-rewrite-plan.md index 6011bedb..365d1233 100644 --- a/docs/rust-rewrite-plan.md +++ b/docs/rust-rewrite-plan.md @@ -191,7 +191,7 @@ Nothing diverges silently. | 28 | The `metadata.json` reader takes strict JSON where Python's `json.loads` took its extensions: `NaN`/`Infinity` literals, numbers beyond f64 range (`1e400`, which Python reads as `inf`), and lone-surrogate `\uD800` escapes all read as corruption, so a file carrying one is quarantined to `metadata.json.corrupt` (bytes intact, single slot) and the run starts with an empty cache where Python loaded the records around the value. | serde_json cannot represent a lone surrogate in a Rust `String` and f64 cannot hold `1e400`, so accepting them would mean values the store could not round-trip; no build of dl (Python or Rust) ever writes any of them — only a hand-edited or third-party-written file can carry one — and the quarantine keeps the bytes recoverable. Pinned by `pythons_json_extensions_read_as_corruption_here` in `domain/metadata.rs`. | | 29 | A clone the host refused as not-found gets a second line naming the same repository under an owner the completion cache knows: `Did you mean 'kinisi-robotics/kinisi_ros'? …`. Python printed git's stderr and stopped. The line is added only for a host's own not-found wording (GitHub, GitLab, Bitbucket), only from a clone step, and only when the cache holds the same repo name under another owner *and* not under the owner typed; at most three candidates are named and any remainder is counted. The git text above it is unchanged in every case. | A mistyped owner and a revoked permission produce the same six lines from git, so git's words cannot tell them apart — and devlaunch already holds the answer in the list its own shell completion offers. Additive: nothing is removed or reworded, and a machine with no cache entry sees exactly what it saw before. The suggestion names a *spec*, not a `dl …` command line, because `aid` renders through the same function and its own invocation carries an agent prompt. Pinned by the `wrong_owner_hint` tests in `dl/tests/launch.rs` and `dl/src/render.rs`. | -| 30 | `--stop` and `--rm` are **appendable**: on a line that already said something else they win, and the words they displace are reported rather than refused. `dl 'review this pr' --rm` was `Unknown command 'review this pr'` (exit 1) in both Python and earlier Rust; `dl prune --force --rm` took `prune` as the *workspace* and reported it unknown. A leading verb word is now skipped when finding the target, a word spelling the flag's own verb is absorbed silently, and everything else displaced is named on stderr (`--rm overrode the rest of the line: 'review this pr' was not acted on.`) before anything is removed. `aid` peels the same two flags — plus a `--force` in their company — off the end of its argv before the prompt join, so `aid --rm --force` removes the workspace and hands no agent command to dl; a verb flag before the spec is collected the same way rather than passed through beside a `--` tail dl would refuse. The peel is bounded to the exact words at the very end of the line, so a prompt that mentions `--rm` or ends in a bare `--force` is untouched, and a `--` command tail cannot be overridden at all. | **Superseded by row 32**, which takes both spellings back: `--rm` became docker's `--rm` and `--stop` was retired, so no flag overrides a line any more. A shell makes appending to a recalled line cheap and rewriting its front expensive, so "and now delete it" has to be spellable as a suffix. The notice is what makes it safe rather than a silent discard: the line may now carry an instruction it will not carry out, so a deliberate suffix and a slip have to be distinguishable. Row 15 made the flag spellings work; this makes them the one form that can be typed last. Pinned by the `suffix_verb` tests in `dl/src/cli.rs` and `aid/src/rewrite.rs`. It was also pinned by the `--aid -- --help` allowance in the parity harness's case list — aid's help text is hand-written and was byte-compared against Python's, so documenting a flag Python's aid had not got was where this row became visible to that harness — and the whole harness retired with the Python tree (#267), so the Rust tests are what is left. | +| 30 | `--stop` and `--rm` are **appendable**: on a line that already said something else they win, and the words they displace are reported rather than refused. `dl 'review this pr' --rm` was `Unknown command 'review this pr'` (exit 1) in both Python and earlier Rust; `dl prune --force --rm` took `prune` as the *workspace* and reported it unknown. A leading verb word is now skipped when finding the target, a word spelling the flag's own verb is absorbed silently, and everything else displaced is named on stderr (`--rm overrode the rest of the line: 'review this pr' was not acted on.`) before anything is removed. `aid` peels the same two flags — plus a `--force` in their company — off the end of its argv before the prompt join, so `aid --rm --force` removes the workspace and hands no agent command to dl; a verb flag before the spec is collected the same way rather than passed through beside a `--` tail dl would refuse. The peel is bounded to the exact words at the very end of the line, so a prompt that mentions `--rm` or ends in a bare `--force` is untouched, and a `--` command tail cannot be overridden at all. | **Superseded by row 32**, which takes both spellings back: `--rm` became docker's `--rm` and `--stop` was retired, so no flag overrides a line any more. A shell makes appending to a recalled line cheap and rewriting its front expensive, so "and now delete it" has to be spellable as a suffix. The notice is what makes it safe rather than a silent discard: the line may now carry an instruction it will not carry out, so a deliberate suffix and a slip have to be distinguishable. Row 15 made the flag spellings work; this makes them the one form that can be typed last. It was pinned by the `suffix_verb` tests in `dl/src/cli.rs` and `aid/src/rewrite.rs`, and by the `--aid -- --help` allowance in the parity harness's case list — aid's help text is hand-written and was byte-compared against Python's, so documenting a flag Python's aid had not got was where this row became visible to that harness. Neither is still standing: the harness retired with the Python tree (#267), and the `suffix_verb` tests were deleted along with the behaviour itself, in the change row 32 records. Nothing pins this row now, which is what superseded means here; row 32 names what pins the grammar that replaced it. | | 31 | `prune` is retired as a workspace verb. `dl prune` / `dl prune ` / `dl prune` were Python's second spelling of `rm` and now refuse with `'prune' is no longer a workspace verb. Use 'dl rm' to delete a workspace, or 'dl --prune' to remove the clone directories no workspace opens any more.` (exit 1). The word stays in a `RETIRED` table rather than being dropped, so it is still never read as a workspace name: `dl prune --force --rm` removes `` (row 30's suffix form, with `prune` absorbed as the same verb the flag asks for), and a live verb beside it still wins from either position, so `dl stop prune` stops a workspace named `prune` as before. `dl --prune` is unchanged. Row 15's parenthetical about the `Ignoring --devcontainer` diagnostic naming `rm` for the `prune` spelling is now unreachable. | **Superseded in part by row 32**, which retires row 30's suffix form: `dl prune --rm` is now this row's refusal rather than a removal of ``, and the word is still in `RETIRED` and still never read as a workspace name. One word meant two unrelated commands, separated only by two dashes: `dl prune` deleted a workspace and `dl --prune` deletes clone directories and no workspace. The two failure modes were losing a workspace meant to be kept, and being refused by `--prune takes no workspace` for a reason that sentence cannot explain. `rm` has no such twin, and the retirement is a refusal that names both meanings rather than a silent removal. Pinned by the `prune_is_refused_as_a_verb_from_either_position`, `a_retired_word_is_never_read_as_the_workspace` and `a_recalled_prune_line_is_the_retirement_and_the_pair_is_named_ahead_of_it` tests in `dl/src/cli.rs`. | diff --git a/rust/aid/Cargo.toml b/rust/aid/Cargo.toml index 942e3ee9..902bb26b 100644 --- a/rust/aid/Cargo.toml +++ b/rust/aid/Cargo.toml @@ -8,12 +8,19 @@ repository.workspace = true [dependencies] # The only devlaunch crate named here, and deliberately: aid is a command-line -# rewrite that hands the result to dl, so the whole of devlaunch it can reach is -# dl's entry point and the quoting that composes a command line for it. Python -# pinned the same rule as a test (`test_aid` walked aid.py's AST and refused any -# use of `dl` beyond `main`, `get_version` and `DL_VALUE_OPTIONS`, and retired -# with the Python tree in #267); here it is the dependency graph, which needs no -# test. +# rewrite that hands the result to dl, so what it can reach is whatever dl makes +# public and nothing else. That is the whole of what this line enforces. aid +# cannot name `devlaunch-core` or `devlaunch-runner`, so a new reach into either +# has to be a `pub use` somebody adds to dl and reviews there. +# +# It is a looser rule than Python's, which is worth saying rather than glossing: +# `test_aid` walked aid.py's AST and refused any use of `dl` beyond `main`, +# `get_version` and `DL_VALUE_OPTIONS` (that suite retired with the Python tree +# in #267). Nothing counts names here, and the count is well past three: aid's +# source reaches dl's signal handlers, terminal helpers, version markers and +# rendering, and some of what it reaches is devlaunch-core's own arriving +# through a dl re-export (`shell`, `env_str`). The surface is a review question +# now, not a test. dl = { path = "../dl" } # Forwarded rather than declared, so the two binaries cannot report different From 5d61b8ca288828b1c534baa995ab355674729089 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Sat, 29 Aug 2026 19:00:47 +0100 Subject: [PATCH 4/5] The guard's own resolver took a directory for a file Both from the review bot and both worth taking: a citation these patterns match is spelled like a file and the failure message tells you to open one, so a directory answering to the name satisfied `exists` and nothing a reader wants. And the changelog said sixty where the scope says sixty-six. --- CHANGELOG.md | 4 ++-- test/test_citations_resolve.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c83d115..0ac14ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the dead branch was written straight back, with every fallback that would have answered correctly sitting unreachable behind it. -- **Sixty citations that pointed at nothing now point at something, and a guard - keeps it that way.** Comments across `rust/` name the test that pins the +- **Sixty-six citations that pointed at nothing now point at something, and a + guard keeps it that way.** Comments across `rust/` name the test that pins the behaviour they describe, which is most of what makes them worth reading. Retiring the Python implementation (#267) deleted about forty of the files those names referred to, in one commit, and the names stayed. A pointer into diff --git a/test/test_citations_resolve.py b/test/test_citations_resolve.py index ee5c0d17..580a5fb2 100644 --- a/test/test_citations_resolve.py +++ b/test/test_citations_resolve.py @@ -110,8 +110,12 @@ def sources() -> list[Path]: def _resolves(citation: str) -> bool: + # `is_file` rather than `exists`: every citation these patterns match is + # spelled like a file, and the failure message tells the reader to open one. + # A directory that happens to answer to the name would satisfy `exists` and + # nothing a reader wants. if "/" in citation: - return (REPO_ROOT / citation).exists() + return (REPO_ROOT / citation).is_file() return citation in TEST_FILES From ea4c4c61d024f34600924e64dd242c989a1797d4 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Sat, 29 Aug 2026 20:35:21 +0100 Subject: [PATCH 5/5] docs: keep the changelog entry under [Unreleased] across the 0.25.0 cut --- CHANGELOG.md | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d09e63b..023a9555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,26 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.25.0] - 2026-08-28 - ### Fixed -- **A cache whose default branch was deleted upstream no longer records the dead - name.** A bare clone remembers the default branch it was cloned with, in its - `HEAD` symref, and nothing repoints it afterwards: `dl` has never written a - bare's `HEAD`. So a repository that renamed `master` to `main` leaves the cache - pointing at `refs/heads/master` long after the prune that deleted the ref, and - `git symbolic-ref HEAD` keeps answering `master`, exit 0, because a symbolic ref - is a name rather than a branch. - - The reading now checks that the name is a ref the clone really has, and treats a - name that is not as it treats a refusal: ask the next probe. The cost is one - local `show-ref` per clone or adopt, and what it buys is that no default branch - `dl` records was read off a ref that is not there. It bit hardest on the adopt - path, which rebuilds a record by reading the clone: delete `metadata.json` and - the dead branch was written straight back, with every fallback that would have - answered correctly sitting unreachable behind it. - - **Sixty-six citations that pointed at nothing now point at something, and a guard keeps it that way.** Comments across `rust/` name the test that pins the behaviour they describe, which is most of what makes them worth reading. @@ -52,6 +34,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `docs/rust-port-scope.md` are out of scope, being records of what was true when they were written. +## [0.25.0] - 2026-08-28 + +### Fixed + +- **A cache whose default branch was deleted upstream no longer records the dead + name.** A bare clone remembers the default branch it was cloned with, in its + `HEAD` symref, and nothing repoints it afterwards: `dl` has never written a + bare's `HEAD`. So a repository that renamed `master` to `main` leaves the cache + pointing at `refs/heads/master` long after the prune that deleted the ref, and + `git symbolic-ref HEAD` keeps answering `master`, exit 0, because a symbolic ref + is a name rather than a branch. + + The reading now checks that the name is a ref the clone really has, and treats a + name that is not as it treats a refusal: ask the next probe. The cost is one + local `show-ref` per clone or adopt, and what it buys is that no default branch + `dl` records was read off a ref that is not there. It bit hardest on the adopt + path, which rebuilds a record by reading the clone: delete `metadata.json` and + the dead branch was written straight back, with every fallback that would have + answered correctly sitting unreachable behind it. + - **A session that dies badly no longer takes your terminal with it.** A terminal is not only a stream: a full screen program switches modes on in the emulator for its own use, the kitty keyboard protocol, bracketed paste, mouse reporting, the