Skip to content

feat: add SSHTest connection preflight diagnostics - #22

Merged
uhs-robert merged 9 commits into
mainfrom
feat/ssh-test-preflight
Sep 10, 2026
Merged

uhs-robert merged 9 commits into
mainfrom
feat/ssh-test-preflight

Conversation

@uhs-robert

@uhs-robert uhs-robert commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a read-only :SSHTest preflight command for diagnosing SSH connections before mounting.

  • accepts the same ad-hoc host syntax as :SSHConnect
  • uses the existing host picker when no argument is provided
  • reports the exact ssh -G command and the same batch-authentication command used by SSHConnect
  • preserves explicit user and port values through the real SSH authentication and remote-home-resolution paths
  • reports resolved SSH parameters, exit codes, stdout, and stderr
  • shows the prospective SSHFS command when the remote path is known, without executing it
  • prepares the ControlMaster socket directory through the same path used by SSHConnect
  • preserves any pre-existing ControlMaster connection and cleans up a socket created only for SSHTest
  • displays results in a disposable scratch buffer
  • does not create an SSHFS mount, mount directory, lockfile, or persistent diagnostic connection state
  • exposes require("sshfs").test(host) for programmatic use

Example

:SSHTest production
:SSHTest deploy@example.com
:SSHTest deploy@example.com -p 2222

Design

SSHTest and SSHConnect share SSH command builders for batch authentication, interactive authentication, remote home resolution, and ControlMaster management. This keeps the diagnostic path aligned with the production connection path instead of maintaining a second implementation.

The preflight records the ControlMaster pid before authentication. If a master already exists, it is preserved. A master is closed only when none existed beforehand, the batch command itself connected successfully, and the pid is unchanged at cleanup time. That prevents the preflight from tearing down a connection created by a concurrent :SSHConnect while it was running. This keeps the command diagnostic-only without breaking an existing shared SSH session.

The diagnostic intentionally does not execute the SSHFS mount. Running sshfs simply to obtain an exit code would mutate system/plugin state. When a remote path is supplied, the report shows the SSHFS command that SSHConnect would build, using a <mount-point> placeholder because SSHTest does not create the local mount directory.

Testing

Regression coverage is included, on top of the shared harness from #25. Run it with make test.

  • the shared SSH command builders: batch probe, remote-home lookup, interactive authentication, and control operations
  • explicit user/port propagation through each builder, and their omission when unset
  • control_master_pid parsing for a running master, a missing socket, and unparseable output
  • report rendering: resolved configuration summary, joined identity files, pass/fail with exit codes and stderr, the prospective sshfs command shown but never executed, tilde resolution through the remote home, the socket directory failure path, and the disposable scratch buffer
  • ControlMaster lifecycle: a master the preflight created is closed, a pre-existing one is preserved, and one that appeared from a concurrent :SSHConnect or was replaced mid-test is left alone

Closes #12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The diagnostics output currently drops repeated ssh -G keys (e.g., multiple identityfile entries) and the surfaced SSHFS option ordering is non-deterministic, which can make reported commands inaccurate/unstable.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a new read-only :SSHTest preflight diagnostic flow to help users validate SSH connectivity, resolved config, and derived commands before attempting an SSHFS mount, while refactoring shared SSH/SSHFS command construction to keep diagnostics aligned with the real connect/mount path.

Changes:

  • Introduces sshfs.diagnostic to run ssh -G, batch-auth, and optional remote-home resolution, then render a scratch-buffer report.
  • Refactors SSH command assembly in sshfs.lib.ssh to accept {name,user,port} host objects and exposes reusable builders (batch/auth/home/control).
  • Exposes require("sshfs").test(host) and adds the :SSHTest user command entry point, plus a reusable Sshfs.build_mount_command(...) builder.
File summaries
File Description
lua/sshfs/lib/sshfs.lua Extracts a reusable SSHFS command builder and updates call sites to pass full host objects through auth/home flows.
lua/sshfs/lib/ssh.lua Adds host normalization + shared SSH command builders for batch/auth/home/control and updates APIs to accept host objects.
lua/sshfs/init.lua Exposes test on the public module surface.
lua/sshfs/diagnostic.lua New diagnostic runner/report renderer for :SSHTest (ssh -G, auth, home resolution, prospective sshfs command).
lua/sshfs/api.lua Adds Api.test and registers the :SSHTest user command.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lua/sshfs/diagnostic.lua
Comment thread lua/sshfs/lib/sshfs.lua
@uhs-robert
uhs-robert force-pushed the feat/ssh-test-preflight branch from c1cba99 to 95668d5 Compare August 30, 2026 12:44

Copy link
Copy Markdown
Owner Author

Proposed merge order

To avoid creating a circular dependency around #24, I suggest treating #24 as the shared test-infrastructure epic rather than requiring one monolithic test PR to contain tests for code that has not merged yet.

  1. test: add automated test framework and regression coverage #24 test foundation first — land the minimal test harness + CI plumbing on main with baseline coverage for existing behavior.
  2. fix: support fuse-t on macOS #20 next — rebase onto the test foundation, add its fuse-t/SSHFS-version regression coverage using the shared harness, then merge.
  3. fix: surface connection failures and clean failed mounts #21 next — rebase onto updated main, add the connection-failure/cleanup regression cases tracked by test: add automated test framework and regression coverage #24, then merge.
  4. feat: add SSHTest connection preflight diagnostics #22 after that — rebase this PR onto updated main, add the SSHTest command-builder, rendering, repeated identityfile, deterministic SSHFS command, and ControlMaster lifecycle coverage tracked by test: add automated test framework and regression coverage #24, then merge.
  5. feat: add configurable debug logging #23 last — rebase the debug-logging PR onto the final SSH/auth architecture from fix: surface connection failures and clean failed mounts #21/feat: add SSHTest connection preflight diagnostics #22 so its instrumentation targets the settled code paths and avoids repeated conflict resolution in api.lua, init.lua, and ssh.lua.

This keeps #24 as the common testing requirement while avoiding tests for unmerged feature behavior being forced into a prerequisite PR. #24 can remain open until the required regression suites for #20/#21/#22 are all represented.

@uhs-robert
uhs-robert force-pushed the feat/ssh-test-preflight branch from 759fd05 to 11a941a Compare August 30, 2026 12:46
`ssh -G` prints the entire resolved configuration, which added roughly
85 lines and buried the test results. The "Resolved SSH configuration"
section already summarizes the relevant fields, so stdout is now
suppressed for that step while stderr is still shown.
Cleanup previously ran whenever no master existed before the test, which
races with a concurrent :SSHConnect: a socket created in that window was
closed by the preflight, tearing down the other connection. Replace
has_control_master with control_master_pid and claim ownership only when
no master existed beforehand and the batch command connected
successfully, then verify the pid is unchanged before sending -O exit.
Adds a dependency-free unit test harness that runs inside headless
Neovim, plus regression coverage for the behavior that exists on main.

The harness provides describe/it, deep-equality and error assertions,
and a runner (`nvim -l tests/run.lua`, wrapped as `make test`) that
exits non-zero on failure so CI can gate on it. A stub module replaces
the handful of system-facing calls the plugin makes (vim.fn.system,
vim.fn.executable, vim.system, vim.notify) and restores them per case,
so no test contacts a real SSH server or mount table.

Baseline suites cover mount table parsing for Linux fuse.sshfs, macFUSE
and findmnt output, mount directory creation, SSH command construction
including tilde expansion and quote escaping, ad-hoc host string
parsing, configuration merging with its deprecation shims, and remote
path mapping.

CI runs the suite and stylua --check on every pull request.

Refs #24
A call under test can return more than one value, and keeping only the
first silently turned assertions on the later ones into assertions on
nil.
Adds the regression suite this PR describes, on top of the shared
harness.

Covers the SSH command builders that SSHTest shares with SSHConnect:
the batch probe forces a master and disables prompts, the home lookup
reuses the socket without renegotiating one, interactive
authentication stays promptable, and control operations address the
socket. Explicit user and port values propagate through all of them and
are omitted when unset.

Covers control_master_pid parsing for a running master, a missing
socket, and unparseable output.

Covers the report itself: the resolved configuration summary, joined
identity files, the omission of the raw ssh -G dump, pass and fail
rendering with exit codes and stderr, the prospective sshfs command
(shown but never executed), tilde resolution through the remote home,
the socket directory failure path, and the disposable scratch buffer.

Covers ControlMaster lifecycle: a master the preflight created is
closed, a pre-existing one is preserved, and one that appeared from a
concurrent connect or was replaced mid-test is left alone.

Refs #24
The assertion that SSHTest never runs sshfs was too broad. Once the
fuse-t work in #20 lands, build_mount_command probes `sshfs --version`
to decide whether to render 3.x or 2.x cache option names, so the
preflight legitimately spawns sshfs without mounting anything. Narrow
the assertion to what it was meant to catch: an actual mount.
@uhs-robert
uhs-robert marked this pull request as ready for review September 10, 2026 22:49
@uhs-robert
uhs-robert merged commit e041e11 into main Sep 10, 2026
2 checks passed
@uhs-robert
uhs-robert deleted the feat/ssh-test-preflight branch September 10, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEAT: Add a :SSHTest preflight command to show resolved parameters and the exact commands/exit codes for a given host.

2 participants