From 1bf90a57c9cf84dfc369666d7d022a24156ef51d Mon Sep 17 00:00:00 2001 From: Leo-Paul Goffic Date: Thu, 16 Jul 2026 17:05:40 +0200 Subject: [PATCH] fix(tests): mock updater version in tests --- cli/src/commands/update.ts | 10 ++- cli/src/lib/installed-version.ts | 25 ++++++ cli/src/lib/updater.ts | 14 ++-- cli/tests/updater.test.ts | 130 ++++++++++++++++++------------- 4 files changed, 114 insertions(+), 65 deletions(-) create mode 100644 cli/src/lib/installed-version.ts diff --git a/cli/src/commands/update.ts b/cli/src/commands/update.ts index 6df9443..71f643e 100644 --- a/cli/src/commands/update.ts +++ b/cli/src/commands/update.ts @@ -1,9 +1,9 @@ -import { VERSION } from "@/version.ts"; import { asCliArgString } from "@/lib/cli-args.ts"; import { writeCommandOutput } from "@/lib/command-output.ts"; import { defineAltertableCommand } from "@/lib/command-context.ts"; import { CliError } from "@/lib/errors.ts"; import { GLOBAL_ARGV_FLAGS_WITH_VALUE, isGlobalArgvFlag } from "@/lib/global-flags.ts"; +import { getInstalledVersion } from "@/lib/installed-version.ts"; import type { OutputSink } from "@/lib/runtime.ts"; import { checkForUpdate, @@ -76,9 +76,9 @@ function buildTargetResult(targetVersion: string): UpdateCheckResult { } const source = resolveUpdateSource(); return { - current_version: VERSION, + current_version: getInstalledVersion(), latest_version: version, - update_available: compareVersions(version, VERSION) > 0, + update_available: compareVersions(version, getInstalledVersion()) > 0, source, release_url: releaseUrlForSource(source, version), checked_at: new Date().toISOString(), @@ -121,7 +121,9 @@ export async function executeUpdateCommand( targetVersion && compareVersions(result.latest_version, result.current_version) < 0 ) { - throw new CliError(`Target version v${result.latest_version} is older than v${VERSION}.`); + throw new CliError( + `Target version v${result.latest_version} is older than v${getInstalledVersion()}.`, + ); } if (!args.force && !result.update_available) { await writeUpdateCheck(result, targetVersion, sink); diff --git a/cli/src/lib/installed-version.ts b/cli/src/lib/installed-version.ts new file mode 100644 index 0000000..4c7ac74 --- /dev/null +++ b/cli/src/lib/installed-version.ts @@ -0,0 +1,25 @@ +import { VERSION } from "@/version.ts"; + +let installedVersionOverride: string | undefined; + +export function getInstalledVersion(): string { + return installedVersionOverride ?? VERSION; +} + +export function withInstalledVersion(version: string, fn: () => T): T { + const previous = installedVersionOverride; + installedVersionOverride = version; + try { + const result = fn(); + if (result instanceof Promise) { + return result.finally(() => { + installedVersionOverride = previous; + }) as T; + } + installedVersionOverride = previous; + return result; + } catch (error) { + installedVersionOverride = previous; + throw error; + } +} diff --git a/cli/src/lib/updater.ts b/cli/src/lib/updater.ts index 9f1a943..9ffe7d9 100644 --- a/cli/src/lib/updater.ts +++ b/cli/src/lib/updater.ts @@ -5,7 +5,8 @@ import { createHash, randomBytes } from "node:crypto"; import { spawnSync } from "node:child_process"; import type { CliContext } from "@/context.ts"; import { isJsonOutput } from "@/context.ts"; -import { USER_AGENT, VERSION } from "@/version.ts"; +import { USER_AGENT } from "@/version.ts"; +import { getInstalledVersion } from "@/lib/installed-version.ts"; import { configDir, configGetGlobal, configSetGlobal } from "@/lib/config.ts"; import { urlencode } from "@/lib/encode.ts"; import { CliError, HttpError, NetworkError } from "@/lib/errors.ts"; @@ -1051,9 +1052,9 @@ export async function checkForUpdate(options: FetchLatestOptions = {}): Promise< const release = await fetchLatestRelease(options); const checkedAt = new Date().toISOString(); const result: UpdateCheckResult = { - current_version: VERSION, + current_version: getInstalledVersion(), latest_version: release.version, - update_available: compareVersions(release.version, VERSION) > 0, + update_available: compareVersions(release.version, getInstalledVersion()) > 0, source: release.source, release_url: release.releaseUrl, checked_at: checkedAt, @@ -1141,7 +1142,10 @@ function writeAutomaticUpdateNotice(sink: OutputSink, latestVersion: string): vo sink.writeMetadata([ "", renderDisplayText([ - span(`Update available: altertable ${latestVersion} (current ${VERSION}).`, "subtle"), + span( + `Update available: altertable ${latestVersion} (current ${getInstalledVersion()}).`, + "subtle", + ), ]), renderDisplayText([ span(`Run ${recommendedInstallCommand(latestVersion)} to install it.`, "subtle"), @@ -1177,7 +1181,7 @@ export async function maybeShowUpdateNotice(options: AutomaticNoticeOptions): Pr } } - if (latestVersion && compareVersions(latestVersion, VERSION) > 0) { + if (latestVersion && compareVersions(latestVersion, getInstalledVersion()) > 0) { writeAutomaticUpdateNotice(sink, latestVersion); } } catch { diff --git a/cli/tests/updater.test.ts b/cli/tests/updater.test.ts index 3c6fb6d..3d35e02 100644 --- a/cli/tests/updater.test.ts +++ b/cli/tests/updater.test.ts @@ -15,6 +15,7 @@ import { CLI_PACKAGE_METADATA } from "@/package-metadata.ts"; import { VERSION } from "@/version.ts"; import { ConfigurationError } from "@/lib/errors.ts"; import { resolveProcessExecutablePath } from "@/lib/executable-path.ts"; +import { withInstalledVersion } from "@/lib/installed-version.ts"; import { checkForUpdate, compareVersions, @@ -47,6 +48,9 @@ import { createCliRuntime, runWithCliRuntime } from "@/lib/runtime.ts"; let testHome = ""; const packageJsonPath = resolve(import.meta.dir, "../package.json"); const UPDATE_TEST_VERSION = "1.2.3"; +// Pin the "installed" version well below UPDATE_TEST_VERSION so update checks +// stay meaningful regardless of what release-please stamps into version.ts. +const INSTALLED_TEST_VERSION = "1.0.0"; const LINUX_X64_ASSET = "altertable-linux-x64"; const LINUX_X64_DOWNLOAD_URL = `https://download.example/${LINUX_X64_ASSET}`; const CHECKSUMS_DOWNLOAD_URL = "https://download.example/checksums.txt"; @@ -303,13 +307,15 @@ describe("release discovery", () => { }); test("checkForUpdate writes cached state", async () => { - const result = await checkForUpdate({ - source: "npm", - fetchImpl: jsonFetch({ version: UPDATE_TEST_VERSION }), - }); + await withInstalledVersion(INSTALLED_TEST_VERSION, async () => { + const result = await checkForUpdate({ + source: "npm", + fetchImpl: jsonFetch({ version: UPDATE_TEST_VERSION }), + }); - expect(result.update_available).toBe(true); - expect(readUpdateState().latest_version).toBe(UPDATE_TEST_VERSION); + expect(result.update_available).toBe(true); + expect(readUpdateState().latest_version).toBe(UPDATE_TEST_VERSION); + }); }); }); @@ -696,14 +702,16 @@ describe("automatic update checks", () => { stderr.push(...lines); }; - await runWithCliRuntime(runtime, async () => { - await maybeShowUpdateNotice({ - context: { debug: false, json: false, agent: false }, - commandName: "context", - stderrIsTTY: true, - fetchImpl: jsonFetch({ version: UPDATE_TEST_VERSION }), - }); - }); + await withInstalledVersion(INSTALLED_TEST_VERSION, () => + runWithCliRuntime(runtime, async () => { + await maybeShowUpdateNotice({ + context: { debug: false, json: false, agent: false }, + commandName: "context", + stderrIsTTY: true, + fetchImpl: jsonFetch({ version: UPDATE_TEST_VERSION }), + }); + }), + ); expect(stderr.join("\n")).toContain(`Update available: altertable ${UPDATE_TEST_VERSION}`); expect(stderr.join("\n")).toContain("altertable update"); @@ -714,26 +722,28 @@ describe("automatic update checks", () => { const stderr: string[] = []; let fetchCalls = 0; - await maybeShowUpdateNotice({ - context: { debug: false, json: false, agent: false }, - commandName: "help", - stderrIsTTY: true, - fetchImpl: (async () => { - fetchCalls += 1; - throw new Error("cached notices should not fetch"); - }) as unknown as typeof fetch, - sink: { - json: false, - debug: false, - writeStderr() {}, - writeJson() {}, - writeRaw() {}, - writeHuman() {}, - writeMetadata(lines) { - stderr.push(...lines); + await withInstalledVersion(INSTALLED_TEST_VERSION, () => + maybeShowUpdateNotice({ + context: { debug: false, json: false, agent: false }, + commandName: "help", + stderrIsTTY: true, + fetchImpl: (async () => { + fetchCalls += 1; + throw new Error("cached notices should not fetch"); + }) as unknown as typeof fetch, + sink: { + json: false, + debug: false, + writeStderr() {}, + writeJson() {}, + writeRaw() {}, + writeHuman() {}, + writeMetadata(lines) { + stderr.push(...lines); + }, }, - }, - }); + }), + ); expect(fetchCalls).toBe(0); expect(stderr.join("\n")).toContain(`Update available: altertable ${UPDATE_TEST_VERSION}`); @@ -744,26 +754,28 @@ describe("automatic update checks", () => { const state = readUpdateState(); const stderr: string[] = []; - await maybeShowUpdateNotice({ - context: { debug: false, json: false, agent: false }, - commandName: "context", - stderrIsTTY: true, - now: new Date(Date.parse(state.last_checked_at ?? "") + 24 * 60 * 60 * 1000), - fetchImpl: (async () => { - throw new Error("network unavailable"); - }) as unknown as typeof fetch, - sink: { - json: false, - debug: false, - writeStderr() {}, - writeJson() {}, - writeRaw() {}, - writeHuman() {}, - writeMetadata(lines) { - stderr.push(...lines); + await withInstalledVersion(INSTALLED_TEST_VERSION, () => + maybeShowUpdateNotice({ + context: { debug: false, json: false, agent: false }, + commandName: "context", + stderrIsTTY: true, + now: new Date(Date.parse(state.last_checked_at ?? "") + 24 * 60 * 60 * 1000), + fetchImpl: (async () => { + throw new Error("network unavailable"); + }) as unknown as typeof fetch, + sink: { + json: false, + debug: false, + writeStderr() {}, + writeJson() {}, + writeRaw() {}, + writeHuman() {}, + writeMetadata(lines) { + stderr.push(...lines); + }, }, - }, - }); + }), + ); expect(stderr.join("\n")).toContain(`Update available: altertable ${UPDATE_TEST_VERSION}`); expect(readUpdateState().last_error).toBe("Unable to check for CLI updates."); @@ -772,11 +784,13 @@ describe("automatic update checks", () => { describe("update command", () => { test("checks an explicit target version without network", async () => { - const output = await runUpdateCommand(["update", UPDATE_TEST_VERSION, "--check"]); + const output = await withInstalledVersion(INSTALLED_TEST_VERSION, () => + runUpdateCommand(["update", UPDATE_TEST_VERSION, "--check"]), + ); expect(output.stdout[0]).toBe( [ - `A new version of altertable is available: v${UPDATE_TEST_VERSION} (current v${VERSION})`, + `A new version of altertable is available: v${UPDATE_TEST_VERSION} (current v${INSTALLED_TEST_VERSION})`, `Run ${UpdaterConfig.commands.selfUpdate} to install it.`, ].join("\n"), ); @@ -803,7 +817,9 @@ describe("update command", () => { }); test("upgrade aliases update", async () => { - const output = await runUpdateCommand(["upgrade", UPDATE_TEST_VERSION, "--check"]); + const output = await withInstalledVersion(INSTALLED_TEST_VERSION, () => + runUpdateCommand(["upgrade", UPDATE_TEST_VERSION, "--check"]), + ); expect(output.stdout[0]).toContain( `A new version of altertable is available: v${UPDATE_TEST_VERSION}`, @@ -831,7 +847,9 @@ describe("update command", () => { }, }; - await executeUpdateCommand({}, runtime.output, dependencies); + await withInstalledVersion(INSTALLED_TEST_VERSION, () => + executeUpdateCommand({}, runtime.output, dependencies), + ); expect(installedVersion).toBe(UPDATE_TEST_VERSION); expect(output.stderr).toContain(`Updating altertable to ${UPDATE_TEST_VERSION}`);