Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/__tests__/version-check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { compareSemver, getUpdateType, checkForUpdates } from '../utils/version-check';
import { logger } from '../utils/logger';

vi.mock('../utils/logger', () => ({
logger: {
error: vi.fn(),
},
}));

describe('version-check utilities', () => {
describe('compareSemver', () => {
it('should return "lt" if a < b', () => {
expect(compareSemver('1.0.0', '1.0.1')).toBe('lt');
expect(compareSemver('1.0.0', '1.1.0')).toBe('lt');
expect(compareSemver('1.0.0', '2.0.0')).toBe('lt');
});

it('should return "gt" if a > b', () => {
expect(compareSemver('1.0.1', '1.0.0')).toBe('gt');
expect(compareSemver('1.1.0', '1.0.0')).toBe('gt');
expect(compareSemver('2.0.0', '1.0.0')).toBe('gt');
});

it('should return "eq" if a === b', () => {
expect(compareSemver('1.0.0', '1.0.0')).toBe('eq');
expect(compareSemver('v1.0.0', '1.0.0')).toBe('eq');
});
});

describe('getUpdateType', () => {
it('should return "major" for major updates', () => {
expect(getUpdateType('1.0.0', '2.0.0')).toBe('major');
});

it('should return "minor" for minor updates', () => {
expect(getUpdateType('1.0.0', '1.1.0')).toBe('minor');
});

it('should return "patch" for patch updates', () => {
expect(getUpdateType('1.0.0', '1.0.1')).toBe('patch');
});

it('should return empty string if versions are equal', () => {
expect(getUpdateType('1.0.0', '1.0.0')).toBe('');
});

it('should return empty string for downgrades', () => {
expect(getUpdateType('2.0.0', '1.0.0')).toBe('');
expect(getUpdateType('1.1.0', '1.0.0')).toBe('');
expect(getUpdateType('1.0.1', '1.0.0')).toBe('');
});
});

describe('checkForUpdates', () => {
const fetchMock = vi.fn();

beforeEach(() => {
vi.stubGlobal('fetch', fetchMock);
vi.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('should log an error if registry response is invalid', async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ not_a_version: '1.2.3' }),
});

await checkForUpdates();

expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Invalid registry response'));
});

it('should handle fetch timeout/error gracefully', async () => {
fetchMock.mockRejectedValue(new Error('Timeout'));

await checkForUpdates();
// Should not throw and should not log error (it fails silently in the catch block for network errors)
expect(logger.error).not.toHaveBeenCalled();
});

it('should log update message if a newer version is available', async () => {
fetchMock.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ version: '9.9.9' }),
});

const consoleSpy = vi.spyOn(console, 'log');
await checkForUpdates();

expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('update available!'));
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('v9.9.9'));
});
});
});
23 changes: 11 additions & 12 deletions src/utils/version-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chalk from 'chalk';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { logger } from './logger';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand All @@ -18,7 +19,7 @@ function getInstalledVersion(): string {
}

// Compare two semver strings; returns 'lt' if a < b, 'gt' if a > b, 'eq' if equal
function compareSemver(a: string, b: string): 'lt' | 'gt' | 'eq' {
export function compareSemver(a: string, b: string): 'lt' | 'gt' | 'eq' {
const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number);
const [a1, a2, a3] = parse(a);
const [b1, b2, b3] = parse(b);
Expand All @@ -29,23 +30,15 @@ function compareSemver(a: string, b: string): 'lt' | 'gt' | 'eq' {
}

// Determine update type for messaging
function getUpdateType(installed: string, latest: string): string {
export function getUpdateType(installed: string, latest: string): string {
const cmp = compareSemver(installed, latest);
if (cmp === 'eq') return '';
const [i1] = installed.replace(/^v/, '').split('.').map(Number);
const [l1] = latest.replace(/^v/, '').split('.').map(Number);
if (l1 > i1) return 'major';
function getUpdateType(installed: string, latest: string): string {
const cmp = compareSemver(installed, latest);
if (cmp === 'eq') return '';
if (cmp === 'eq' || cmp === 'gt') return '';
const [i1 = 0, i2 = 0] = installed.replace(/^v/, '').split('.').map(Number);
const [l1 = 0, l2 = 0] = latest.replace(/^v/, '').split('.').map(Number);
if (l1 > i1) return 'major';
if (l2 > i2) return 'minor';
return 'patch';
}
return 'patch';
}

export async function checkForUpdates(): Promise<void> {
try {
Expand All @@ -54,7 +47,13 @@ export async function checkForUpdates(): Promise<void> {
});
if (!response.ok) return;

const data = await response.json() as { version: string };
const data = (await response.json()) as unknown;

if (!data || typeof data !== 'object' || !('version' in data) || typeof data.version !== 'string') {
logger.error('Invalid registry response: version not found');
return;
}

const latestVersion = data.version;
const installedVersion = getInstalledVersion();

Expand Down
Loading