Skip to content

fix(install): clean up stale symlinks and detect manifest spec drift#1

Merged
ascarter merged 1 commit into
mainfrom
fix/install-correctness
Jun 6, 2026
Merged

fix(install): clean up stale symlinks and detect manifest spec drift#1
ascarter merged 1 commit into
mainfrom
fix/install-correctness

Conversation

@ascarter

@ascarter ascarter commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Two related correctness fixes for the bin/man/completions install flow. Both surface pre-existing latent bugs that are independent of any particular asset class.

1. Reap stale symlinks before re-extracting

installFromAsset wiped the tool's ToolDir and recreated symlinks for everything in the current manifest entry — but never removed the previous install's symlinks first. If the user renamed a bin spec (e.g. bin = [\"old\"]bin = [\"new\"]) or removed a man/completion entry, the old symlinks in BinDir/ManDir/completion dirs remained, dangling into the now-deleted ToolDir.

Fix: call m.removeToolSymlinks(name) at the top of installFromAsset, before os.RemoveAll(toolDir). The reaper only removes symlinks whose targets resolve into the tool's ToolDir, so it's safe to call when no prior install exists.

2. Detect manifest asset-spec drift in isUpToDate

isUpToDate compared only the release tag, so adding a new man/completion entry to an already-installed tool (or renaming a bin) at the same release tag was reported as "up to date" and the new symlinks were never created — the user had to know to pass `--force`.

Fix: also compare `state.Bin/Man/Completions` against the manifest entry as unordered sets, and return false when any of them differ. Adds a small `stringSlicesEqual` helper.

Tests

Two regression tests cover the behaviors:

  • `TestInstallFromAssetReapsStaleSymlinks` in `internal/tool/tool_test.go`
  • `TestIsUpToDateDetectsManifestSpecChange` in new `cmd/install_test.go` (plus `TestStringSlicesEqual`)

Full `go vet ./...` and `go test ./...` pass.

Two related correctness fixes for the bin/man/completions install flow.

1. Reap stale symlinks before re-extracting.

   installFromAsset wiped the tool's ToolDir and recreated symlinks
   for everything in the current manifest entry — but never removed
   the previous install's symlinks first. If the user renamed a bin
   spec (e.g. bin = ["old"] → bin = ["new"]) or removed a man/
   completion entry, the old symlinks in BinDir/ManDir/completion
   dirs remained, dangling into the now-deleted ToolDir.

   Fix: call m.removeToolSymlinks(name) at the top of
   installFromAsset, before os.RemoveAll(toolDir). The reaper only
   removes symlinks whose targets resolve into the tool's ToolDir,
   so it's safe to call when no prior install exists.

2. Detect manifest asset-spec drift in isUpToDate.

   isUpToDate compared only the release tag, so adding a new
   man/completion entry to an already-installed tool (or renaming a
   bin) at the same release tag was reported as "up to date" and
   the new symlinks were never created — the user had to know to
   pass --force.

   Fix: also compare state.Bin/Man/Completions against the manifest
   entry as unordered sets, and return false when any of them differ.
   Adds a small stringSlicesEqual helper.

Both fixes are independent of any particular asset class — they apply
equally to bin, man, and completions. Regression tests added for both
behaviors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ascarter

ascarter commented Jun 6, 2026

Copy link
Copy Markdown
Owner Author

@copilot Review this PR

Copilot AI commented Jun 6, 2026

Copy link
Copy Markdown

@copilot Review this PR

The changes look good. Here's my review:

internal/tool/tool.go — stale symlink reaping

  • removeToolSymlinks is called before os.RemoveAll(toolDir), which is the correct ordering: symlinks pointing into the (soon-to-be-deleted) ToolDir are removed first, then the ToolDir itself is wiped.
  • removeToolSymlinks already guards against the no-prior-install case by checking whether each link's resolved target is inside the ToolDir — safe to call unconditionally.

cmd/install.go — manifest spec drift detection

  • isUpToDate now short-circuits on tag mismatch first (cheap check), then falls through to the spec comparison (slightly more expensive copy+sort). Correct and efficient.
  • stringSlicesEqual treats nil and empty as equivalent, which matches how TOML unmarshals omitted fields — no false positives when a manifest omits man vs. sets it to [].
  • The "up to date" console message is printed only when both the tag and spec match, so the UX is consistent with the old behaviour in the common case.

Tests

  • TestInstallFromAssetReapsStaleSymlinks exercises the full round-trip: symlink created by a prior install, then InstallFromAsset called with a different bin spec — the test verifies the old link is gone and the new one exists.
  • TestIsUpToDateDetectsManifestSpecChange covers tag match + spec match, tag match + spec drift (added completions), tag match + renamed bin, and tag mismatch — all four branches.
  • TestStringSlicesEqual covers nil/nil, nil/empty, order-independence, mismatch, and length mismatch.

go vet ./... and go test ./... pass cleanly. No issues found.

@ascarter ascarter merged commit b40b418 into main Jun 6, 2026
5 checks passed
@ascarter ascarter deleted the fix/install-correctness branch June 6, 2026 21:50
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.

2 participants