Skip to content

fix(detector): replace n.Current()'s silent-download call site#88

Merged
dipto0321 merged 1 commit into
mainfrom
fix/detector/n-current
Jul 3, 2026
Merged

fix(detector): replace n.Current()'s silent-download call site#88
dipto0321 merged 1 commit into
mainfrom
fix/detector/n-current

Conversation

@dipto0321

Copy link
Copy Markdown
Owner

Summary

internal/detector/n.go's N.Current() shells out to n current — an undocumented subcommand in upstream tj/n (bin/n). The string current is not a first-class case arm in the script's dispatch block; it falls through to the default *) arm install "$1"; exit ;;. Inside install, "current" matches the version-label branch in display_remote_versions (same as "latest"), which downloads the latest Node.js tarball, extracts it, and activates it. So every Current() invocation would silently side-effect-install the newest available Node version on the user's machine — every run, every n-using machine.

Callers treat Current() errors as "active version unknown, don't exclude it" (the safe-by-convention default per the Manager interface doc). That meant the cleanup safety net never fired on any n install: the active version was always "unknown," and --cleanup / --yes / cfg.Cleanup.Auto could happily auto-delete whatever was actually powering the user's shell.

This PR replaces the call site with <N_PREFIX>/bin/node --version. n's activate function (bin/n) copies the active node binary into that path, so its --version IS the active version — side-effect-free, and matches every supported n install. The pre-fix tests (TestParseNCurrent_*, TestNCurrent_InvokesShell) codified the buggy behavior; they're deleted.

Linked issues

Fixes #59

Type of change

  • fix — bug fix (PATCH bump)
  • feat — new feature (MINOR bump)
  • refactor — no behavior change
  • perf — performance improvement
  • docs — documentation only
  • test — tests only
  • chore — maintenance / dependency bump
  • ci — CI/CD only
  • build — build system only
  • Breaking change (MAJOR bump) — explain below

Checklist

Screenshots / output

Pre-fix behavior on an n-using machine, every nodeup invocation:

$ nodeup upgrade --cleanup
# in n.go: Current() calls `n current`
# upstream `n current` resolves to "latest", downloads
# the new tarball, and activates it. The user's machine
# now has a version they didn't ask for.
# Then the all-or-nothing cleanup deletes whichever
# version happens to be on PATH, including the user's
# currently-active Node.js.

Post-fix behavior:

$ nodeup upgrade --cleanup
# in n.go: Current() runs `/usr/local/bin/node --version`
# — pure read, no side effects.
# If /usr/local/bin/node is missing, the error propagates
# and the upgrade flow treats Current() as "unknown,"
# which (combined with #58's ForcePerVersion safety net)
# means per-version y/N guards every delete.

Files touched

  • internal/detector/n.go
    • N.Current() rewritten to shell out to <N_PREFIX>/bin/node --version via the existing nPrefix() helper. The pre-fix runShell(ctx, "n", "current") is gone.
    • parseNCurrent (the pre-fix n current parser) is renamed to parseNNodeVersion and its doc comment is rewritten to explain <node --version>'s output format and why n current is not used.
    • The mutation-methods doc block (the long comment listing every Current strategy) is updated to spell out the reasoning and link fix(detector): n manager's Current() relies on unverified 'n current' subcommand #59.
  • internal/detector/n_test.go
    • Tests TestParseNCurrent_Bare, TestParseNCurrent_WithPrefix, TestParseNCurrent_Empty, TestNCurrent_InvokesShell (which all pinned n current invocation / parsing): deleted.
    • New tests in their place:
      • TestParseNNodeVersion_VPrefixedReal, TestParseNNodeVersion_Bare, TestParseNNodeVersion_LeadingTrailingWhitespace, TestParseNNodeVersion_Empty, TestParseNNodeVersion_Unparseable — parser coverage for both common shapes (vX.Y.Z) and defensive shapes (bare X.Y.Z, whitespace, garbage).
      • TestN_Current_InvokesNodeVersionNotNCurrent — integration test that fails the test if the literal n current request re-appears in any future regression. The fixture's t.Fatalf cites the request so the diagnosis is immediate.
      • TestN_Current_PropagatesRunShellError, TestN_Current_PropagatesParseError — error-path coverage.
    • New import: "strings".
  • CHANGELOG.md### Fixed entry under [Unreleased] documenting the change with the fix(detector): n manager's Current() relies on unverified 'n current' subcommand #59 reference.

Pre-fix, N.Current() ran `n current` — an undocumented subcommand
in upstream tj/n (bin/n). `n current` is not a first-class case
arm in the dispatch block; it falls through to the default `*`
arm which calls `install "$1"`. Inside `install`, the string
"current" matches the version-label branch in
display_remote_versions (same branch as "latest"), and the
function downloads the latest Node.js tarball, extracts it, and
calls activate. So every Current() invocation would silently
side-effect-install the newest available Node version behind
the user's back — every run, on every n-using machine.

Callers treat Current() errors as "active version unknown, don't
exclude it" (the safe-by-convention default per the Manager
interface doc), so this also silently defeated the cleanup
safety net on every n install.

Replacement: shell out to `$N_PREFIX/bin/node --version` and
parse the output. n's `activate` function copies the active
node binary into $N_PREFIX/bin/node, so that binary's
--version IS the active version. Side-effect-free, matches
every supported n install, and the parser handles both vX.Y.Z
and bare X.Y.Z output shapes (the latter is defensive against
n forks).

Tests:

- TestParseNNodeVersion_* (4): parser correctness for v-prefixed,
  bare, whitespace-padded, empty, and unparseable input. The
  pre-fix parser's name was parseNCurrent — the rename is the
  most visible signal that the contract changed.
- TestN_Current_InvokesNodeVersionNotNCurrent: pins the new
  behavior at the integration level. The literal "n current"
  request is the ONE THING the fixture asserts MUST NOT appear;
  any regression that re-introduces the bug fails immediately
  with a t.Fatalf citing the request.
- TestN_Current_PropagatesRunShellError: missing node binary
  or exec failure surfaces as an error so callers treat it
  as "active unknown".
- TestN_Current_PropagatesParseError: blank --version output
  errors rather than returning the zero semver.

The pre-fix tests (TestParseNCurrent_*, TestNCurrent_InvokesShell)
are deleted — they codified the buggy behavior.

Refs #59
@cocogitto-bot

cocogitto-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

✔️ 01c528a - Conventional commits check succeeded.

@dipto0321 dipto0321 merged commit df0c958 into main Jul 3, 2026
10 checks passed
@dipto0321 dipto0321 deleted the fix/detector/n-current branch July 3, 2026 03:36
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.

fix(detector): n manager's Current() relies on unverified 'n current' subcommand

1 participant