From 4efb421e33f3204d8421e2407a3aaa4e08f2edfc Mon Sep 17 00:00:00 2001 From: rUv Date: Fri, 23 Oct 2026 12:48:58 -0400 Subject: [PATCH] fix(update): never treat an older registry version as an update (downgrade on 3.60.0) (#4432) getUpdateType compared major, minor and patch independently. With 3.60.0 running and 3.59.17 as the registry latest, the minor check failed but patch(17) > patch(0) returned 'patch', so the startup check auto-installed 3.59.17: a downgrade. It also made 'ruflo init' run npm install in the project and time out the mods-cli-bin test on the 3.60.0 release PR. An update is a strictly newer version: guard with semver.gt first. Co-authored-by: r --- .../update-checker-no-downgrade.test.ts | 31 +++++++++++++++++++ v3/@claude-flow/cli/src/update/checker.ts | 6 ++-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 v3/@claude-flow/cli/__tests__/update-checker-no-downgrade.test.ts diff --git a/v3/@claude-flow/cli/__tests__/update-checker-no-downgrade.test.ts b/v3/@claude-flow/cli/__tests__/update-checker-no-downgrade.test.ts new file mode 100644 index 0000000000..d64a2ef94f --- /dev/null +++ b/v3/@claude-flow/cli/__tests__/update-checker-no-downgrade.test.ts @@ -0,0 +1,31 @@ +/** + * getUpdateType compared major, minor and patch independently, so a CLI AHEAD of the registry's `latest` in minor but behind it in + * patch (3.60.0 running, latest 3.59.17) read as a 'patch' update (17 > 0) and the startup check auto-installed the older version: + * a downgrade. An update is a strictly newer version, and only that. + */ +import { describe, expect, it } from 'vitest'; +import { getUpdateType } from '../src/update/checker.js'; + +describe('getUpdateType', () => { + it('never reports a version that is not newer as an update (the 3.60.0 vs 3.59.17 downgrade)', () => { + expect(getUpdateType('3.60.0', '3.59.17')).toBe('none'); + expect(getUpdateType('3.58.0', '3.57.2')).toBe('none'); + expect(getUpdateType('4.0.0', '3.99.99')).toBe('none'); + expect(getUpdateType('3.59.17', '3.59.16')).toBe('none'); + expect(getUpdateType('3.59.17', '3.59.17')).toBe('none'); + expect(getUpdateType('3.7.0-alpha.2', '3.7.0-alpha.1')).toBe('none'); + }); + + it('still names a newer version by how far ahead it is', () => { + expect(getUpdateType('3.59.16', '3.59.17')).toBe('patch'); + expect(getUpdateType('3.59.17', '3.60.0')).toBe('minor'); + expect(getUpdateType('3.59.17', '3.60.3')).toBe('minor'); + expect(getUpdateType('3.59.17', '4.0.0')).toBe('major'); + expect(getUpdateType('3.9.9', '3.10.0')).toBe('minor'); + }); + + it('reads an invalid version as no update', () => { + expect(getUpdateType('latest', '3.59.17')).toBe('none'); + expect(getUpdateType('3.59.17', 'next')).toBe('none'); + }); +}); diff --git a/v3/@claude-flow/cli/src/update/checker.ts b/v3/@claude-flow/cli/src/update/checker.ts index e98023e1fe..055453d875 100644 --- a/v3/@claude-flow/cli/src/update/checker.ts +++ b/v3/@claude-flow/cli/src/update/checker.ts @@ -81,7 +81,7 @@ async function fetchPackageInfo(packageName: string): Promise 0, and the startup check then auto-installed the older version: a downgrade. + if (!semver.gt(latest, current)) { return 'none'; }