From aee322ea88e39ffb57a3b427016f5cd1339141cf Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 30 May 2026 13:42:31 -0400 Subject: [PATCH] fix: compare cliproxy fork release suffixes (#1419) --- .../__tests__/version-checker-suffix.test.ts | 21 +++++++ src/cliproxy/binary/version-checker.ts | 59 +++++++++++++------ tests/unit/ui/cliproxy-version-risk.test.ts | 4 +- ui/src/lib/cliproxy-version-risk.ts | 45 ++++++++++---- 4 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 src/cliproxy/binary/__tests__/version-checker-suffix.test.ts diff --git a/src/cliproxy/binary/__tests__/version-checker-suffix.test.ts b/src/cliproxy/binary/__tests__/version-checker-suffix.test.ts new file mode 100644 index 00000000..186942b0 --- /dev/null +++ b/src/cliproxy/binary/__tests__/version-checker-suffix.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'bun:test'; + +import { compareCliproxyVersions, isNewerVersion } from '../version-checker'; + +describe('cliproxy version comparison', () => { + it('treats missing fork release suffix as zero', () => { + expect(compareCliproxyVersions('6.6.81', '6.6.81-0')).toBe(0); + expect(isNewerVersion('6.6.81-0', '6.6.81')).toBe(false); + }); + + it('orders patched fork release suffixes after core version equality', () => { + expect(compareCliproxyVersions('7.1.31-1', '7.1.31-0')).toBe(1); + expect(compareCliproxyVersions('7.1.31-0', '7.1.31-1')).toBe(-1); + expect(isNewerVersion('7.1.31-1', '7.1.31-0')).toBe(true); + }); + + it('lets core version precedence win before fork release suffixes', () => { + expect(compareCliproxyVersions('7.1.32-0', '7.1.31-99')).toBe(1); + expect(isNewerVersion('7.1.31-99', '7.1.32-0')).toBe(false); + }); +}); diff --git a/src/cliproxy/binary/version-checker.ts b/src/cliproxy/binary/version-checker.ts index cfade244..f4e7c547 100644 --- a/src/cliproxy/binary/version-checker.ts +++ b/src/cliproxy/binary/version-checker.ts @@ -33,28 +33,49 @@ interface FetchAllVersionsDeps { fetchJsonFn?: typeof fetchJson; } +interface ParsedCliproxyVersion { + major: number; + minor: number; + patch: number; + forkRelease: number; +} + +function parseCliproxyVersion(version: string): ParsedCliproxyVersion { + const normalized = version.trim().replace(/^v/, ''); + const [coreVersion, forkReleaseValue = '0'] = normalized.split('-', 2); + const [major = 0, minor = 0, patch = 0] = coreVersion + .split('.') + .map((part) => parseInt(part, 10) || 0); + const forkRelease = /^\d+$/.test(forkReleaseValue) ? parseInt(forkReleaseValue, 10) || 0 : 0; + + return { major, minor, patch, forkRelease }; +} + +function compareVersionPart(a: number, b: number): number { + if (a > b) return 1; + if (a < b) return -1; + return 0; +} + +export function compareCliproxyVersions(a: string, b: string): number { + const left = parseCliproxyVersion(a); + const right = parseCliproxyVersion(b); + + return ( + compareVersionPart(left.major, right.major) || + compareVersionPart(left.minor, right.minor) || + compareVersionPart(left.patch, right.patch) || + compareVersionPart(left.forkRelease, right.forkRelease) + ); +} + /** - * Compare semver versions (true if latest > current) - * Handles CLIProxyAPIPlus versioning: strips -0 suffix before comparison + * Compare CLIProxy release versions (true if latest > current). + * Missing fork suffixes are treated as -0, while patched fork suffixes such as + * 7.1.31-1 still sort newer than 7.1.31-0. */ export function isNewerVersion(latest: string, current: string): boolean { - // Strip -0 suffix from CLIProxyAPIPlus versions (e.g., "6.6.40-0" -> "6.6.40") - const cleanLatest = latest.replace(/-\d+$/, ''); - const cleanCurrent = current.replace(/-\d+$/, ''); - - const latestParts = cleanLatest.split('.').map((p) => parseInt(p, 10) || 0); - const currentParts = cleanCurrent.split('.').map((p) => parseInt(p, 10) || 0); - - // Pad arrays to same length - while (latestParts.length < 3) latestParts.push(0); - while (currentParts.length < 3) currentParts.push(0); - - for (let i = 0; i < 3; i++) { - if (latestParts[i] > currentParts[i]) return true; - if (latestParts[i] < currentParts[i]) return false; - } - - return false; // Equal versions + return compareCliproxyVersions(latest, current) > 0; } /** diff --git a/tests/unit/ui/cliproxy-version-risk.test.ts b/tests/unit/ui/cliproxy-version-risk.test.ts index b44c398f..36738dd7 100644 --- a/tests/unit/ui/cliproxy-version-risk.test.ts +++ b/tests/unit/ui/cliproxy-version-risk.test.ts @@ -6,9 +6,11 @@ import { } from '../../../ui/src/lib/cliproxy-version-risk'; describe('cliproxy-version-risk helpers', () => { - it('compares versions while ignoring release suffixes', () => { + it('compares fork release suffixes after core versions', () => { expect(compareCliproxyVersions('6.6.88', '6.6.81-0')).toBe(1); expect(compareCliproxyVersions('6.6.81-0', '6.6.81')).toBe(0); + expect(compareCliproxyVersions('7.1.31-1', '7.1.31-0')).toBe(1); + expect(compareCliproxyVersions('7.1.31-0', '7.1.31-1')).toBe(-1); expect(compareCliproxyVersions('6.6.80', '6.6.81')).toBe(-1); }); diff --git a/ui/src/lib/cliproxy-version-risk.ts b/ui/src/lib/cliproxy-version-risk.ts index 3e55ea2e..73adf225 100644 --- a/ui/src/lib/cliproxy-version-risk.ts +++ b/ui/src/lib/cliproxy-version-risk.ts @@ -1,20 +1,39 @@ -function normalizeVersionParts(version: string): number[] { - return version.replace(/-\d+$/, '').split('.').map(Number); +interface ParsedCliproxyVersion { + major: number; + minor: number; + patch: number; + forkRelease: number; +} + +function parseCliproxyVersion(version: string): ParsedCliproxyVersion { + const normalized = version.trim().replace(/^v/, ''); + const [coreVersion, forkReleaseValue = '0'] = normalized.split('-', 2); + const [major = 0, minor = 0, patch = 0] = coreVersion + .split('.') + .map((part) => Number.parseInt(part, 10) || 0); + const forkRelease = /^\d+$/.test(forkReleaseValue) + ? Number.parseInt(forkReleaseValue, 10) || 0 + : 0; + + return { major, minor, patch, forkRelease }; +} + +function compareVersionPart(a: number, b: number): number { + if (a > b) return 1; + if (a < b) return -1; + return 0; } export function compareCliproxyVersions(a: string, b: string): number { - const aParts = normalizeVersionParts(a); - const bParts = normalizeVersionParts(b); + const left = parseCliproxyVersion(a); + const right = parseCliproxyVersion(b); - for (let index = 0; index < 3; index += 1) { - const aPart = aParts[index] || 0; - const bPart = bParts[index] || 0; - - if (aPart > bPart) return 1; - if (aPart < bPart) return -1; - } - - return 0; + return ( + compareVersionPart(left.major, right.major) || + compareVersionPart(left.minor, right.minor) || + compareVersionPart(left.patch, right.patch) || + compareVersionPart(left.forkRelease, right.forkRelease) + ); } export function isCliproxyVersionExperimental(version: string, maxStableVersion: string): boolean {