Files
hikari-desktop/src/lib/components/CliVersion.test.ts
T
hikari 45c1caa133 feat: add CLI update check with npm registry indicator
On app start, check the npm registry for the latest
@anthropic-ai/claude-code version and compare against the installed
version. If behind, the CLI badge turns amber with a pulsing up-arrow
and a tooltip advising how to update.

Also bumps SUPPORTED_CLI_VERSION to 2.1.72.
2026-03-10 11:48:19 -07:00

183 lines
5.9 KiB
TypeScript

/**
* CliVersion Component Tests
*
* Tests the version comparison logic used by the CliVersion component,
* which compares the installed CLI version against the supported version.
*
* What this component does:
* - Displays the installed Claude CLI version
* - Displays the highest audited/supported CLI version
* - Shows a warning when the installed version is ahead of or behind supported
*
* Manual testing checklist:
* - [ ] Installed version is fetched and displayed on mount
* - [ ] "current" badge shows in green when versions match
* - [ ] "ahead" badge shows in amber when installed is newer than supported
* - [ ] "behind" badge shows in red when installed is older than supported
* - [ ] Warning message appears for "ahead" and "behind" states
*/
import { describe, it, expect } from "vitest";
const SUPPORTED_CLI_VERSION = "2.1.72";
function compareVersions(a: string, b: string): number {
const aParts = a.split(".").map(Number);
const bParts = b.split(".").map(Number);
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aVal = aParts[i] ?? 0;
const bVal = bParts[i] ?? 0;
if (aVal > bVal) return 1;
if (aVal < bVal) return -1;
}
return 0;
}
// ---
describe("SUPPORTED_CLI_VERSION", () => {
it("is defined and non-empty", () => {
expect(SUPPORTED_CLI_VERSION).toBeTruthy();
});
it("matches the expected audited version", () => {
expect(SUPPORTED_CLI_VERSION).toBe("2.1.72");
});
});
describe("compareVersions", () => {
describe("equal versions", () => {
it("returns 0 for identical versions", () => {
expect(compareVersions("1.0.0", "1.0.0")).toBe(0);
});
it("returns 0 for the supported CLI version against itself", () => {
expect(compareVersions(SUPPORTED_CLI_VERSION, SUPPORTED_CLI_VERSION)).toBe(0);
});
it("returns 0 for 0.0.0 vs 0.0.0", () => {
expect(compareVersions("0.0.0", "0.0.0")).toBe(0);
});
});
describe("major version differences", () => {
it("returns 1 when a has a higher major version", () => {
expect(compareVersions("2.0.0", "1.0.0")).toBe(1);
});
it("returns -1 when a has a lower major version", () => {
expect(compareVersions("1.0.0", "2.0.0")).toBe(-1);
});
});
describe("minor version differences", () => {
it("returns 1 when a has a higher minor version", () => {
expect(compareVersions("1.2.0", "1.1.0")).toBe(1);
});
it("returns -1 when a has a lower minor version", () => {
expect(compareVersions("1.1.0", "1.2.0")).toBe(-1);
});
});
describe("patch version differences", () => {
it("returns 1 when a has a higher patch version", () => {
expect(compareVersions("1.0.2", "1.0.1")).toBe(1);
});
it("returns -1 when a has a lower patch version", () => {
expect(compareVersions("1.0.1", "1.0.2")).toBe(-1);
});
});
describe("major version takes precedence", () => {
it("returns 1 when a has a higher major but lower minor", () => {
expect(compareVersions("2.0.0", "1.9.9")).toBe(1);
});
it("returns -1 when a has a lower major but higher minor", () => {
expect(compareVersions("1.9.9", "2.0.0")).toBe(-1);
});
});
describe("unequal segment counts", () => {
it("treats missing segments as 0 (a shorter than b)", () => {
expect(compareVersions("1.0", "1.0.0")).toBe(0);
});
it("treats missing segments as 0 (a longer than b)", () => {
expect(compareVersions("1.0.0", "1.0")).toBe(0);
});
it("correctly compares when a has an extra non-zero segment", () => {
expect(compareVersions("1.0.1", "1.0")).toBe(1);
});
it("correctly compares when b has an extra non-zero segment", () => {
expect(compareVersions("1.0", "1.0.1")).toBe(-1);
});
});
describe("supported CLI version comparisons", () => {
it("returns 1 for a version ahead of supported", () => {
expect(compareVersions("2.2.0", SUPPORTED_CLI_VERSION)).toBe(1);
});
it("returns -1 for a version behind supported", () => {
expect(compareVersions("2.1.0", SUPPORTED_CLI_VERSION)).toBe(-1);
});
it("returns 0 for exactly the supported version", () => {
expect(compareVersions("2.1.72", SUPPORTED_CLI_VERSION)).toBe(0);
});
});
});
// Mirrors the updateAvailable derived logic in CliVersion.svelte
function isUpdateAvailable(installedVersion: string, latestNpmVersion: string | null): boolean {
if (!latestNpmVersion || installedVersion === "Loading..." || installedVersion === "Unknown") {
return false;
}
const semverMatch = /(\d+\.\d+\.\d+)/.exec(installedVersion);
if (!semverMatch) return false;
return compareVersions(semverMatch[1], latestNpmVersion) < 0;
}
describe("updateAvailable", () => {
it("returns false when latestNpmVersion is null", () => {
expect(isUpdateAvailable("2.1.70", null)).toBe(false);
});
it("returns false when installed is Loading...", () => {
expect(isUpdateAvailable("Loading...", "2.1.72")).toBe(false);
});
it("returns false when installed is Unknown", () => {
expect(isUpdateAvailable("Unknown", "2.1.72")).toBe(false);
});
it("returns false when installed equals latest", () => {
expect(isUpdateAvailable("2.1.72", "2.1.72")).toBe(false);
});
it("returns false when installed is ahead of latest", () => {
expect(isUpdateAvailable("2.1.73", "2.1.72")).toBe(false);
});
it("returns true when installed is behind latest", () => {
expect(isUpdateAvailable("2.1.70", "2.1.72")).toBe(true);
});
it("returns true when installed has a lower minor version", () => {
expect(isUpdateAvailable("2.0.99", "2.1.72")).toBe(true);
});
it("handles version strings with extra info like '2.1.70 (build 123)'", () => {
expect(isUpdateAvailable("2.1.70 (build 123)", "2.1.72")).toBe(true);
});
it("returns false for unparseable installed version", () => {
expect(isUpdateAvailable("not-a-version", "2.1.72")).toBe(false);
});
});