Report only what doctor's checks actually establish - #7
Conversation
Motivation: Copilot's review of stellar#2670 found three `doctor` diagnostics claiming more than the data behind them supports, each able to send a user after a cause that was never observed. Behavior: - A check whose fetch failed still stamps the cache, but leaves the recorded versions untouched, so "last refreshed by" credited an install with version data it never fetched. Say "checked" instead, in the messages and the field docs: the writer paces the next check rather than vouching for the versions stored beside it. Recording it only after a successful fetch was the alternative, and it hides the install worth finding -- a stale one whose fetch fails still suppresses everyone else's check for a day. - The single-install branch printed a count without the listing, so the one case a listing would settle was the one case that omitted path and version. List every discovered install. - Absent agreement was reported as disagreement: two executables that cannot be run are both unknown, yet the message blamed differing versions. `InstalledVersions` now keeps Agreed, Disagree and Unanswered apart. An observed disagreement still wins over a failed probe alongside it, because that one is a fact. Tests: four unit cases for `summarize_versions`, and two integration cases driving real subprocesses through a fake `PATH` -- two unrunnable CLIs, and a disagreement sitting next to an unreadable binary. The lone-install listing and the reworded cache-writer lines are asserted too. Release impact: no breaking change and no migration. The `upgrade_check.json` shape is untouched and older files still load. Only `doctor`'s stderr wording changes -- no command, flag or help text does, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
b783bcf to
b7df621
Compare
pedro-pelicioni
left a comment
There was a problem hiding this comment.
I cloned the fork, read the code, ran the tests and reproduced doctor by hand.
What checks out
The diagnosis behind all three items is correct, and I verified the first one at the source: in cmd/soroban-cli/src/upgrade_check.rs:132-141 the failed-fetch branch sets latest_check_time and last_checked_by without touching max_stable_version / max_version. So "refreshed" really was false and "checked" is the right word.
It also lines up with what Copilot raised: the 3 suppressed comments on the 2026-08-05 review of stellar/stellar-cli#2670 are exactly these three items.
What I ran locally:
| Command | Result |
|---|---|
cargo test -p soroban-cli --lib doctor:: |
8 passed (324 filtered, so the 332 in the description adds up) |
cargo fmt --all -- --check |
clean |
cargo clippy -p soroban-cli --lib --all-features |
clean |
The summarize_versions logic is correct. Unanswered(0) is only reachable for an empty list, which the (0, _) arm already handles, so no unreachable-looking state escapes.
Issues
1. Three integration tests fail on macOS
Not a theory, this is the actual run on this branch:
test result: FAILED. 7 passed; 3 failed
doctor::does_not_blame_differing_versions_when_a_version_could_not_be_read
doctor::reports_the_running_executable_and_a_lone_install
doctor::warns_when_installs_report_different_versions
find_installs canonicalizes the path (cmd/soroban-cli/src/commands/doctor.rs:345), but the assertions build the expected string from the raw bin_dir. On macOS the temp dir lands under /var/folders/..., which is a symlink to /private/var/..., so the string comparison never matches.
warns_when_installs_report_different_versions already failed before this PR, but the two new path assertions inherited the same flaw and turned 1 failure into 3. One-line fix in empty_dir, which I tested and it takes all 10 to green:
fn empty_dir(sandbox: &TestEnv, name: &str) -> PathBuf {
let dir = sandbox.dir().join(name);
fs::create_dir_all(&dir).unwrap();
dir.canonicalize().unwrap_or(dir)
}Worth fixing the "Full run, all green" line in the description too, or at least saying which platform it was run on.
2. The Disagree message overreaches in exactly the way this PR is fixing
Running the scenario from the new test by hand:
⚠️ Found 3 Stellar CLI executables on PATH reporting different versions; ...
- .../a/stellar (27.1.0)
- .../b/stellar (unknown version)
- .../a/soroban (22.8.0)
Three executables did not report different versions. Two reported and disagree, the third reported nothing, and the listing right below says so. That is the same class of over-claim the PR exists to kill, just smaller, and reports_a_disagreement_even_when_another_executable_is_unreadable pins that sentence as expected behavior.
The underlying call (an observed disagreement outranks a failed probe) is right. What is missing is keeping the unanswered ones out of the count:
Disagree { unanswered: usize },with the sentence becoming something like "Found 3 ...; the 2 that reported a version do not agree (1 could not be asked)".
3. Unanswered throws away the agreement among those that did answer
Two binaries at 27.1.0 plus one that cannot run:
⚠️ Found 3 Stellar CLI executables on PATH, 1 of which did not report a version, so whether they agree could not be determined:
The two that answered agree, and that is the most useful fact on the line. It is gone. Same shape of fix:
Unanswered { agreed: Option<String>, unanswered: usize },Nits
doctor(&sandbox, Path::new(&path), &data_home)incmd/crates/soroban-test/tests/it/doctor.rs:222:"dir1:dir2"is not aPath. It works because it becomes anOsStrinenv(), but now that there is a test with a multi-entryPATH, the helper should takeimpl AsRef<OsStr>.- The
returnin the(0, _)arm ofcheck_installsis redundant,list_installson an empty slice prints nothing. It breaks the symmetry with the other arms for no gain.
Left out of scope
Copilot's subprocess-timeout comment (doctor.rs:326 on the upstream PR) was never actually answered. Going by the API threading, the reply hanging off it (in_reply_to_id=3715688791) talks about integration tests, not about the timeout. run_version still uses Command::output() with no bound, so a hung binary on PATH hangs doctor forever.
It is a bit ironic on this PR specifically: it builds a whole taxonomy around "did not answer", and the case that genuinely never answers is the one that wedges the command. Separate scope, but worth opening an issue or replying on that thread rather than leaving it looking resolved.
Overall a good, well-argued PR. Item 1 is blocking (red tests); I would fix 2 and 3 before sending this upstream, since they are the PR's own subject matter.
What: canonicalize the sandbox directories the `doctor` tests build their
expected paths from, and take a whole `PATH` value rather than a `Path` in
the test helper.
Why: `find_installs` canonicalizes every executable it discovers, so an
expectation built from an uncanonicalized sandbox compares two spellings
of the same path and finds them unequal as strings. On macOS the temporary
directory sits under `/var/folders`, a symlink to `/private/var`, which
fails three of these tests. `warns_when_installs_report_different_versions`
already did before this branch; the two path assertions added since
inherited the same flaw and turned one failure into three. Linux resolves
nothing there, so local runs and CI stayed green and hid it.
Reproduced on Linux by pointing `TMPDIR` at a symlinked directory: without
the canonicalize the same three fail, with it all ten pass. The doc comment
records why the call is there, since its absence is what let the trap
through twice.
The helper's `path` is a whole `PATH` value, and a multi-entry one is
several paths joined by `:` -- not a path. `Path::new("dir1:dir2")` only
worked because `env` takes it back to an `OsStr`, so ask for
`impl AsRef<OsStr>` and let the multi-entry case stop pretending.
Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: review of stellar#2670 found the taxonomy added in b7df621 over-claiming in two places of its own -- the same class of error it exists to remove, one size smaller. Behavior: - `Disagree` counted every executable found, including ones that never answered. Two reporting different versions beside a third that could not be run printed "Found 3 Stellar CLI executables on PATH reporting different versions", contradicted by the listing directly beneath it, where the third reads "(unknown version)". It now carries how many went unanswered and names only the ones that were heard from: "the 2 that reported a version do not agree (1 could not be asked)". - `Unanswered` discarded what the answering executables established. Two at 27.1.0 beside one that cannot run is a machine whose reachable installs agree, and that was the most useful fact on the line; the message said only that agreement could not be determined. It now carries that version and leads with it, while still declining to call the whole set agreed: a version that was never read cannot be ruled out. Every count now sits next to what it counts, so the sentence can be checked against the listing below it. Also drops the redundant `return` in the zero-install arm. `list_installs` prints nothing for an empty slice, so the early exit bought nothing and only broke the symmetry between arms. Tests: unit cases pin the new payloads, including that an agreement survives a failed probe beside it and that the disagreement count excludes the executable that never answered. An integration case drives the agreement-plus-unreadable scenario through real subprocesses, and the two existing messages that changed are re-pinned. Release impact: no breaking change and no migration -- only `doctor`'s stderr wording moves, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
|
All three are addressed, in two commits kept separate so each is green on its own:
1. The macOS path assertionsTaken as proposed. /// A directory under the sandbox, in the same canonicalized form `doctor`
/// reports paths in.
///
/// `find_installs` canonicalizes every executable it discovers, so an expected
/// path built from an uncanonicalized sandbox never matches: on macOS the
/// temporary directory sits under `/var/folders`, a symlink to `/private/var`,
/// and the two spellings compare unequal as strings.
I did not want to take the fix on trust, so I reproduced the mechanism on Linux by forcing mkdir -p /tmp/repro/private/real && ln -sfn /tmp/repro/private/real /tmp/repro/link
TMPDIR=/tmp/repro/link cargo test -p soroban-test --test it doctor::Without the canonicalize, that fails exactly the three you listed — same names, nothing more, nothing less. With it, all 11 pass. It also lines up with the cause: those three are precisely the tests that build an expected string from the sandbox path ( To be clear about what that is worth: it reproduces the mechanism, not the platform. If something else macOS-specific were also contributing, this would not catch it. Which brings me to the ask below. 2.
|
| Command | Result |
|---|---|
cargo test -p soroban-cli --lib doctor:: |
9 passed (was 8; one added) |
cargo test -p soroban-test --test it doctor:: |
11 passed (was 10; one added) |
same, with TMPDIR symlinked |
11 passed |
3f3d65b8 alone, symlinked TMPDIR |
10 passed |
cargo fmt --all -- --check |
clean |
cargo clippy -p soroban-cli --lib --all-features |
clean |
Scoped to doctor:: above; the full suite is in the description, re-measured from a clean build and now including cargo test --workspace --exclude soroban-test --features additional-libs, which had never been run on this branch. All green.
Linux only — that is the correction to the "Full run, all green" line, which the description now carries, along with the platform it was run on.
Motivation
Copilot's review of the upstream PR (stellar#2670) flagged three places where the new diagnostics claim more than the data behind them supports. All three are real, and each one can send a user chasing a cause that was never observed:
upgrade_check.rsrecordslast_checked_byeven when the fetch to crates.io failed — the attempt still paces the next check — butdoctorreported the file as "last refreshed by" that install. That credits an install with version data it never fetched.check_installsprinted a count without callinglist_installs, so the one case where a listing would settle "which executable is this?" was the one case that omitted path and version. The separateRunning executableline is not necessarily the entryPATHresolves by name, and carries no version.common_version == Nonemeant either "known versions disagree" or "at least one executable could not be asked". Both landed on a message blaming differing versions, so two unrunnable binaries — both listed asunknown version— were reported as a version conflict.A follow-up review then found the fix for (3) committing a smaller version of the same error twice, plus a platform bug in the new tests. Both are addressed here; see Follow-up review below.
Groundwork for stellar#2464.
Behavior
doctorsays "checked", not "refreshed". The cache writer is the last writer/pacer, not the source of the versions stored beside it. Kept that semantics — it is the one that answers "which install is holding the next check back?" — and made the wording match the field name (last_checked_by), in the fourdoctormessages and in the field/struct docs.Alternative discarded: recording the writer only after a successful refresh. That would make "refreshed by" literally true, but it loses the diagnostic that matters. When a stale install fails its fetch and stamps the file, it still suppresses everyone else's check for 24h; dropping its identity would leave the file crediting whichever install last succeeded, hiding exactly the install a user needs to find.
Every discovered install is listed.
list_installsnow runs for any non-zero count, including one.Undetermined agreement is no longer reported as disagreement, and neither report speaks for an executable that was never heard from.
InstalledVersionsis now:N Stellar CLI executables on PATH, all reporting X:Found N Stellar CLI executables on PATH reporting different versions; an outdated one can report a version that disagrees with 'stellar --version':Found N Stellar CLI executables on PATH; the K that reported a version do not agree (M could not be asked); an outdated one can report a version that disagrees with 'stellar --version':Found N Stellar CLI executables on PATH; every one that answered reports X, but M could not be asked, so a differing version cannot be ruled out:Found N Stellar CLI executables on PATH; none of them reported a version, so whether they agree could not be determined:An observed disagreement still wins when it sits alongside a failed probe — a contradiction between two known versions is a fact, and a third unrunnable binary does not soften it. It does not join it either: only the executables that answered are counted as disagreeing.
Every count now sits next to what it counts, so each sentence can be checked against the listing printed directly beneath it.
Follow-up review
Three items from review of this branch, all fixed:
1. Three integration tests failed on macOS (blocking).
find_installscanonicalizes every executable it discovers, but the assertions built their expected string from the raw sandbox directory. On macOS the temp dir sits under/var/folders, a symlink to/private/var, so the two spellings never compare equal.warns_when_installs_report_different_versionsalready failed before this branch; the two path assertions added since inherited the same flaw and turned one failure into three.empty_dirnow canonicalizes, with a doc comment recording why.2.
Disagreeover-claimed in exactly the way this PR exists to fix. Two executables reporting different versions beside a third that could not be run printed "Found 3 ... reporting different versions", contradicted by the listing right below it, where the third reads(unknown version). Now carriesunansweredand names only the ones that were heard from.3.
Unanswereddiscarded the agreement among those that did answer. Two installs at 27.1.0 beside one that cannot run is a machine whose reachable installs agree — the most useful fact available, and the message dropped it. Now carries that version and leads with it, while still declining to call the whole set agreed.Wording note on (3): the review suggested "the N that reported a version agree on X". That breaks grammatically when only one answered ("the 1 that reported a version agree on 27.1.0"), and that case is reachable, so the message reads "every one that answered reports X" instead — correct for any count without a second branch.
Also from that review: the test helper now takes
impl AsRef<OsStr>rather than&Path, because a multi-entryPATHis several paths joined by:and is not a path; and the redundantreturnin the zero-install arm is gone, sincelist_installsprints nothing for an empty slice.Tests
Unit (
cmd/soroban-cli/src/commands/doctor.rs) — five cases forsummarize_versions: agreement, distinct known versions, probes that returned nothing (both all-unknown and one-unknown), an agreement surviving a failed probe beside it, and a disagreement coexisting with a failed probe (asserting the unanswered one is counted apart).Integration (
cmd/crates/soroban-test/tests/it/doctor.rs) — three end-to-end cases driving real subprocesses through a fakePATH:does_not_blame_differing_versions_when_a_version_could_not_be_read— two unrunnable CLIs; asserts the undetermined message and thatdifferent versionsis absent.reports_a_disagreement_even_when_another_executable_is_unreadable—27.1.0+22.8.0+ one unrunnable across twoPATHentries; asserts the disagreement surfaces and that only the two that answered are counted.reports_the_agreement_among_the_executables_that_answered— two at27.1.0+ one unrunnable; asserts the agreement survives.Also extended
reports_the_running_executable_and_a_lone_installto assert the lone install is listed with its version, and updated the cache-writer assertions to the new wording. Helperwrite_unrunnable_cliwrites a script that fails both version queries.Runs — Linux only
x86_64 Linux, rustc 1.97.1 stable. macOS and Windows were not run; see the caveat below.
cargo fmt --all -- --checkcargo clippy --all-targets(and--features additional-libs)cargo test -p soroban-cli --lib(and--features additional-libs)cargo test -p soroban-test --test it -- doctor:: version:: plugin:: help:: message::cargo test -p soroban-test -- --skip integration::cargo test -p soroban-test --test it doctor::withTMPDIRsymlinkedcargo test --workspace --exclude soroban-test(and--features additional-libs)Counts are totals across every test target the command builds, so they are higher than the single
test resultline you may notice scrolling past.Not run:
make rpc-test(--features it -- integration), which needs a local RPC — nothing here touches RPC paths.The macOS fix is not verified on macOS. I have no Mac. What I did instead was reproduce the mechanism on Linux by forcing
TMPDIRthrough a symlink, which is what/var→/private/varamounts to:Without the canonicalize that fails exactly the three tests reported on macOS — same names, no more, no fewer — and those three are precisely the ones that build an expected string from the sandbox path. With it, all 11 pass. That establishes the mechanism, not the platform: if something else macOS-specific were also contributing, this would not catch it.
Worth noting that CI will not close the gap either.
build-and-test-macosin.github/workflows/rust.ymlis gated on:so a PR from a
fix/*branch never triggers it — which is why the pre-existing macOS failure went unnoticed in the first place, and why this fix will only be exercised once it lands onmain. A confirmation run on a Mac before merge would be valuable.Note for reviewers running these locally: the integration tests execute
target/debug/stellarand do not rebuild it, socargo build --bin stellaris needed first or the assertions run against a stale binary.Release Impact
No breaking changes. No data migration: this change does not alter the
upgrade_check.jsonshape —last_checked_byand its optional fields are unchanged, and files written before it existed still load.User-visible surface is
stellar doctor's stderr diagnostics only: four cache-writer lines reworded from "refreshed" to "checked", new messages for the undetermined and partially-unanswered cases, and the single-install case now followed by a listing. No command, flag, or help text changed, soFULL_HELP_DOCS.mdis untouched. Anything matching ondoctor's exact stderr text would need updating — no such consumer is known in this repo beyond the tests updated here.Checklist
CheckWriter,UpgradeCheck::last_checked_by,show_version_cache_writer,check_installs,InstalledVersionsandsummarize_versionsnow state that the writer paces the next check rather than vouching for the recorded versions, and that a failed probe is counted apart from an observed disagreement. No user-facing docs changed: no CLI surface change, soFULL_HELP_DOCS.mdis unaffected.doctorremains diagnostic-only.PATHfrom a per-test temp dir and the cache viaSTELLAR_DATA_HOME; nothing touches the real config/data dirs, the network, or any key material.Cargo.lockor workflow change; the new tests live in the existingsoroban-testittarget already run by CI.