128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { semverLt, formatUpdateNotice } from '../src/runtime/update-checker.js';
|
|
import type { UpdateCheckResult } from '../src/runtime/update-checker.js';
|
|
|
|
const { execSyncMock } = vi.hoisted(() => ({
|
|
execSyncMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('node:child_process', () => ({
|
|
execSync: execSyncMock,
|
|
}));
|
|
|
|
async function importUpdateChecker() {
|
|
vi.resetModules();
|
|
return import('../src/runtime/update-checker.js');
|
|
}
|
|
|
|
describe('semverLt', () => {
|
|
it('returns true when a < b', () => {
|
|
expect(semverLt('0.0.1', '0.0.2')).toBe(true);
|
|
expect(semverLt('0.1.0', '0.2.0')).toBe(true);
|
|
expect(semverLt('1.0.0', '2.0.0')).toBe(true);
|
|
expect(semverLt('0.0.1-alpha.1', '0.0.1-alpha.2')).toBe(true);
|
|
expect(semverLt('0.0.1-alpha.1', '0.0.1')).toBe(true);
|
|
});
|
|
|
|
it('returns false when a >= b', () => {
|
|
expect(semverLt('0.0.2', '0.0.1')).toBe(false);
|
|
expect(semverLt('1.0.0', '1.0.0')).toBe(false);
|
|
expect(semverLt('2.0.0', '1.0.0')).toBe(false);
|
|
});
|
|
|
|
it('returns false for empty strings', () => {
|
|
expect(semverLt('', '1.0.0')).toBe(false);
|
|
expect(semverLt('1.0.0', '')).toBe(false);
|
|
expect(semverLt('', '')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('formatUpdateNotice', () => {
|
|
beforeEach(() => {
|
|
execSyncMock.mockReset();
|
|
});
|
|
|
|
it('returns empty string when up to date', () => {
|
|
const result: UpdateCheckResult = {
|
|
current: '1.0.0',
|
|
latest: '1.0.0',
|
|
updateAvailable: false,
|
|
checkedAt: new Date().toISOString(),
|
|
registry: 'https://example.com',
|
|
};
|
|
expect(formatUpdateNotice(result)).toBe('');
|
|
});
|
|
|
|
it('returns a notice when update is available', () => {
|
|
const result: UpdateCheckResult = {
|
|
current: '0.0.1',
|
|
latest: '0.1.0',
|
|
updateAvailable: true,
|
|
checkedAt: new Date().toISOString(),
|
|
registry: 'https://example.com',
|
|
};
|
|
const notice = formatUpdateNotice(result);
|
|
expect(notice).toContain('0.0.1');
|
|
expect(notice).toContain('0.1.0');
|
|
expect(notice).toContain('Update available');
|
|
});
|
|
|
|
it('uses @mosaic/mosaic hints for modern installs', async () => {
|
|
execSyncMock.mockImplementation((command: string) => {
|
|
if (command.includes('ls -g --depth=0 --json')) {
|
|
return JSON.stringify({
|
|
dependencies: {
|
|
'@mosaic/mosaic': { version: '0.0.17' },
|
|
'@mosaic/cli': { version: '0.0.16' },
|
|
},
|
|
});
|
|
}
|
|
|
|
if (command.includes('view @mosaic/mosaic version')) {
|
|
return '0.0.18';
|
|
}
|
|
|
|
throw new Error(`Unexpected command: ${command}`);
|
|
});
|
|
|
|
const { checkForUpdate } = await importUpdateChecker();
|
|
const result = checkForUpdate({ skipCache: true });
|
|
const notice = formatUpdateNotice(result);
|
|
|
|
expect(result.current).toBe('0.0.17');
|
|
expect(result.latest).toBe('0.0.18');
|
|
expect(notice).toContain('@mosaic/mosaic@latest');
|
|
expect(notice).not.toContain('@mosaic/cli@latest');
|
|
});
|
|
|
|
it('falls back to @mosaic/mosaic for legacy @mosaic/cli installs when cli is unavailable', async () => {
|
|
execSyncMock.mockImplementation((command: string) => {
|
|
if (command.includes('ls -g --depth=0 --json')) {
|
|
return JSON.stringify({
|
|
dependencies: {
|
|
'@mosaic/cli': { version: '0.0.16' },
|
|
},
|
|
});
|
|
}
|
|
|
|
if (command.includes('view @mosaic/cli version')) {
|
|
throw new Error('not found');
|
|
}
|
|
|
|
if (command.includes('view @mosaic/mosaic version')) {
|
|
return '0.0.17';
|
|
}
|
|
|
|
throw new Error(`Unexpected command: ${command}`);
|
|
});
|
|
|
|
const { checkForUpdate } = await importUpdateChecker();
|
|
const result = checkForUpdate({ skipCache: true });
|
|
const notice = formatUpdateNotice(result);
|
|
|
|
expect(result.current).toBe('0.0.16');
|
|
expect(result.latest).toBe('0.0.17');
|
|
expect(notice).toContain('@mosaic/mosaic@latest');
|
|
});
|
|
});
|