diff --git a/packages/web/server/lib/package-manager.js b/packages/web/server/lib/package-manager.js index 7e8f9195..4fb390f6 100644 --- a/packages/web/server/lib/package-manager.js +++ b/packages/web/server/lib/package-manager.js @@ -655,16 +655,35 @@ export async function getLatestVersion() { /** * Compare semver-like version strings. */ +function parseVersionForComparison(value) { + const normalized = String(value || '').replace(/^v/, '').split('+')[0]; + const prereleaseIndex = normalized.indexOf('-'); + const core = prereleaseIndex >= 0 ? normalized.slice(0, prereleaseIndex) : normalized; + const parts = core.split('.').map((part) => { + const parsed = Number.parseInt(part || '0', 10); + return Number.isFinite(parsed) ? parsed : 0; + }); + + return { + parts, + prerelease: prereleaseIndex >= 0, + }; +} + function compareVersions(left, right) { - const a = String(left || '').replace(/^v/, '').split('.').map((part) => Number.parseInt(part || '0', 10)); - const b = String(right || '').replace(/^v/, '').split('.').map((part) => Number.parseInt(part || '0', 10)); - const length = Math.max(a.length, b.length); + const a = parseVersionForComparison(left); + const b = parseVersionForComparison(right); + const length = Math.max(a.parts.length, b.parts.length); for (let index = 0; index < length; index += 1) { - const diff = (a[index] || 0) - (b[index] || 0); + const diff = (a.parts[index] || 0) - (b.parts[index] || 0); if (diff !== 0) return diff; } + if (a.prerelease !== b.prerelease) { + return a.prerelease ? -1 : 1; + } + return 0; } diff --git a/packages/web/server/lib/package-manager.test.js b/packages/web/server/lib/package-manager.test.js index e8419e42..3eef72e9 100644 --- a/packages/web/server/lib/package-manager.test.js +++ b/packages/web/server/lib/package-manager.test.js @@ -34,14 +34,16 @@ function createFetchMock() { describe('checkForUpdates', () => { let fetchMock; + let originalFetch; beforeEach(() => { fetchMock = createFetchMock(); - vi.stubGlobal('fetch', fetchMock); + originalFetch = globalThis.fetch; + globalThis.fetch = fetchMock; }); afterEach(() => { - vi.unstubAllGlobals(); + globalThis.fetch = originalFetch; }); // --- Scenario: API says update available, npm confirms --- @@ -98,6 +100,21 @@ describe('checkForUpdates', () => { expect(result.available).toBe(false); }); + it('returns available=false when npm only has a prerelease of the current version', async () => { + fetchMock + .when('api.openchamber.dev', Promise.reject(new Error('Network error'))) + .when('registry.npmjs.org', { + ok: true, + json: async () => ({ + 'dist-tags': { latest: '1.10.0-beta.1' }, + }), + }); + + const result = await checkForUpdates({ currentVersion: '1.10.0' }); + + expect(result.available).toBe(false); + }); + it('does not cross-check desktop update claims against npm', async () => { fetchMock .when('api.openchamber.dev', {