0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
import semverGt from 'semver/functions/gt.js';
|
|
import semverValid from 'semver/functions/valid.js';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
getRuntimeNoticeForVersionResponse,
|
|
getRuntimePolicyForVersionResponse,
|
|
isUpdateAvailableForRuntime,
|
|
} from '../../../src/server/routes/versionUtils';
|
|
|
|
/**
|
|
* These tests verify the logic used in src/server/routes/version.ts
|
|
* for determining when to show the update banner.
|
|
*
|
|
* The version route has three key behaviors:
|
|
* 1. Semantic version comparison (not string comparison)
|
|
* 2. Development builds never show update banner
|
|
* 3. Failed fetches are rate-limited to prevent API hammering
|
|
*/
|
|
|
|
// Replicated from version.ts for testing
|
|
function isDevVersion(version: string): boolean {
|
|
return version.includes('development') || version === '0.0.0';
|
|
}
|
|
|
|
function isUpdateAvailable(latestVersion: string | null, currentVersion: string): boolean {
|
|
if (!latestVersion) {
|
|
return false;
|
|
}
|
|
|
|
if (isDevVersion(currentVersion)) {
|
|
return false;
|
|
}
|
|
|
|
if (semverValid(latestVersion) && semverValid(currentVersion)) {
|
|
return semverGt(latestVersion, currentVersion);
|
|
}
|
|
|
|
return latestVersion !== currentVersion;
|
|
}
|
|
|
|
describe('isDevVersion', () => {
|
|
it('should return true for development version strings', () => {
|
|
expect(isDevVersion('0.0.0-development')).toBe(true);
|
|
expect(isDevVersion('1.0.0-development')).toBe(true);
|
|
expect(isDevVersion('development')).toBe(true);
|
|
});
|
|
|
|
it('should return true for 0.0.0', () => {
|
|
expect(isDevVersion('0.0.0')).toBe(true);
|
|
});
|
|
|
|
it('should return false for normal version strings', () => {
|
|
expect(isDevVersion('1.0.0')).toBe(false);
|
|
expect(isDevVersion('0.120.9')).toBe(false);
|
|
expect(isDevVersion('1.2.3-beta.1')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isUpdateAvailable', () => {
|
|
describe('basic version comparison', () => {
|
|
it('should return true when latest version is greater', () => {
|
|
expect(isUpdateAvailable('1.1.0', '1.0.0')).toBe(true);
|
|
expect(isUpdateAvailable('2.0.0', '1.9.9')).toBe(true);
|
|
expect(isUpdateAvailable('1.0.1', '1.0.0')).toBe(true);
|
|
});
|
|
|
|
it('should return false when versions are equal', () => {
|
|
expect(isUpdateAvailable('1.0.0', '1.0.0')).toBe(false);
|
|
expect(isUpdateAvailable('0.120.9', '0.120.9')).toBe(false);
|
|
});
|
|
|
|
it('should return false when current version is greater (user on pre-release)', () => {
|
|
// BUG FIX: User on beta/pre-release should NOT see "downgrade" prompt
|
|
expect(isUpdateAvailable('1.0.0', '1.1.0-beta.1')).toBe(false);
|
|
expect(isUpdateAvailable('1.0.0', '2.0.0-alpha')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('development builds', () => {
|
|
it('should return false for development versions regardless of latest', () => {
|
|
// BUG FIX: Development builds should never show update banner
|
|
expect(isUpdateAvailable('1.0.0', '0.0.0-development')).toBe(false);
|
|
expect(isUpdateAvailable('99.99.99', '0.0.0-development')).toBe(false);
|
|
});
|
|
|
|
it('should return false for 0.0.0 versions', () => {
|
|
expect(isUpdateAvailable('1.0.0', '0.0.0')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('null/missing latest version', () => {
|
|
it('should return false when latestVersion is null', () => {
|
|
expect(isUpdateAvailable(null, '1.0.0')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('pre-release versions', () => {
|
|
it('should correctly compare pre-release versions', () => {
|
|
// Pre-release is less than release
|
|
expect(isUpdateAvailable('1.0.0', '1.0.0-beta.1')).toBe(true);
|
|
expect(isUpdateAvailable('1.0.0', '1.0.0-alpha')).toBe(true);
|
|
|
|
// Later pre-release
|
|
expect(isUpdateAvailable('1.0.0-beta.2', '1.0.0-beta.1')).toBe(true);
|
|
});
|
|
|
|
it('should return false when user is on newer pre-release', () => {
|
|
// User on RC should not be prompted to "update" to stable if RC is newer
|
|
expect(isUpdateAvailable('1.0.0', '1.0.1-rc.1')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should fall back to string comparison for invalid semver (different part counts)', () => {
|
|
// '1.0' is not valid semver (requires 3 parts), so falls back to string comparison
|
|
// Since '1.0.0' !== '1.0', it returns true (strings are different)
|
|
expect(isUpdateAvailable('1.0.0', '1.0')).toBe(true);
|
|
});
|
|
|
|
it('should fall back to string comparison for non-semver versions', () => {
|
|
// If somehow we get non-semver strings, use string inequality
|
|
expect(isUpdateAvailable('abc', 'def')).toBe(true); // Different strings
|
|
expect(isUpdateAvailable('abc', 'abc')).toBe(false); // Same strings
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getRuntimeNoticeForVersionResponse', () => {
|
|
it('should omit runtime notices when runtime warnings are disabled', () => {
|
|
expect(getRuntimeNoticeForVersionResponse('v20.20.2', true)).toBeNull();
|
|
});
|
|
|
|
it('should return applicable runtime notices when warnings are enabled', () => {
|
|
expect(getRuntimeNoticeForVersionResponse('v20.20.2', false)).toMatchObject({
|
|
currentMajor: 20,
|
|
id: 'node20-removal-2026-07-30',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getRuntimePolicyForVersionResponse', () => {
|
|
it('should retain cutoff metadata when runtime warnings are disabled', () => {
|
|
expect(getRuntimePolicyForVersionResponse('v20.20.2')).toEqual({
|
|
supportEndDate: '2026-07-30',
|
|
});
|
|
});
|
|
|
|
it('should omit cutoff metadata for unaffected runtimes', () => {
|
|
expect(getRuntimePolicyForVersionResponse('v22.22.0')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('isUpdateAvailableForRuntime', () => {
|
|
it('should hide runtime-blocked updates from legacy clients', () => {
|
|
expect(isUpdateAvailableForRuntime(true, true)).toBe(false);
|
|
});
|
|
|
|
it('should preserve compatible updates such as Docker image pulls', () => {
|
|
expect(isUpdateAvailableForRuntime(true, false)).toBe(true);
|
|
expect(isUpdateAvailableForRuntime(false, false)).toBe(false);
|
|
});
|
|
});
|