From d2a9378afb3e6c7a6532f025249df8f85a61a2a6 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 5 Aug 2026 16:47:38 -0300 Subject: [PATCH] fix(cli): re-verify running binary version after update install and warn on shadowing local install (#9475) Closes #9475 --- .fakebin-9475/npm | 4 +++ bin/cli/commands/update.mjs | 20 +++++++++++ .../fixes/9475-update-lies-shadowing.md | 1 + .../cli-update-shadow-install-9475.test.ts | 36 +++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100755 .fakebin-9475/npm create mode 100644 changelog.d/fixes/9475-update-lies-shadowing.md create mode 100644 tests/unit/cli-update-shadow-install-9475.test.ts diff --git a/.fakebin-9475/npm b/.fakebin-9475/npm new file mode 100755 index 0000000000..9422990b9c --- /dev/null +++ b/.fakebin-9475/npm @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +if [ "$1" = "view" ]; then echo "3.8.99"; exit 0; fi +if [ "$1" = "install" ]; then echo "added 1 package"; exit 0; fi +exit 0 diff --git a/bin/cli/commands/update.mjs b/bin/cli/commands/update.mjs index 443f9a498b..75f829107d 100644 --- a/bin/cli/commands/update.mjs +++ b/bin/cli/commands/update.mjs @@ -181,6 +181,26 @@ export async function runUpdateCommand(opts = {}) { // --include=optional keeps the optionalDependencies (better-sqlite3, keytar, // tls-client, llmlingua SLM stack) on update so an omit=optional config can't drop them. execSync("npm install -g omniroute@latest --include=optional", { stdio: "inherit" }); + // Trust-but-verify: `npm install -g` exits 0 even when a shadowing local install + // (e.g. ~/node_modules/omniroute ahead of the global prefix on PATH) means the + // binary the user actually runs was not touched. Re-read the running binary's + // version and warn instead of lying about success (#9475). + const afterVersion = await getCurrentVersion(); + if (afterVersion && compareVersions(afterVersion, latest) < 0) { + printError( + `Global install updated to ${latest}, but the running binary still reports ${afterVersion}.`, + ); + console.log( + " A local `node_modules/omniroute` is likely shadowing the global install on PATH.", + ); + console.log(" Diagnose with:"); + console.log(" which -a omniroute"); + console.log(" command -v omniroute"); + console.log(" npm prefix -g"); + console.log(" Then remove the shadowing local copy (e.g. `npm uninstall omniroute` from its directory)"); + console.log(" or reorder PATH so the global bin comes first."); + return 1; + } printSuccess(`Updated to version ${latest}`); printInfo("Run `omniroute --version` to verify."); return 0; diff --git a/changelog.d/fixes/9475-update-lies-shadowing.md b/changelog.d/fixes/9475-update-lies-shadowing.md new file mode 100644 index 0000000000..969d9b4aee --- /dev/null +++ b/changelog.d/fixes/9475-update-lies-shadowing.md @@ -0,0 +1 @@ +- fix(cli): re-verify running binary version after `omniroute update` install and warn instead of lying about success when a local install shadows the global one (#9475) diff --git a/tests/unit/cli-update-shadow-install-9475.test.ts b/tests/unit/cli-update-shadow-install-9475.test.ts new file mode 100644 index 0000000000..bdac2bb0bd --- /dev/null +++ b/tests/unit/cli-update-shadow-install-9475.test.ts @@ -0,0 +1,36 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const update = await import("../../bin/cli/commands/update.mjs"); +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); +const REAL_VERSION = JSON.parse(readFileSync(path.join(REPO_ROOT, "package.json"), "utf-8")).version; +const FAKE_BIN = path.join(REPO_ROOT, ".fakebin-9475"); + +test("runUpdateCommand claims success without verifying the running binary version changed (#9475)", async () => { + const origPath = process.env.PATH; + process.env.PATH = FAKE_BIN + path.delimiter + origPath; + const stdoutLogs: string[] = []; + const origLog = console.log; + console.log = function (...args: unknown[]) { + stdoutLogs.push(args.map(String).join(" ")); + }; + try { + const exitCode = await update.runUpdateCommand({ yes: true, backup: false }); + const realVersion = await update.getCurrentVersion(); + const claimed = stdoutLogs.some((l) => /Updated to version 3\.8\.99/i.test(l)); + if (claimed && exitCode === 0) { + assert.fail( + "runUpdateCommand claimed Updated to version 3.8.99 (exit 0) but the running binary is still " + + realVersion + + " — the resolved/shadowing install was not actually updated. Must re-verify getCurrentVersion() after install or warn the user.", + ); + } + assert.ok(true); + } finally { + console.log = origLog; + process.env.PATH = origPath; + } +});