Files
openchamber/packages/web/server/lib/package-manager.test.js
T

268 lines
7.6 KiB
JavaScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Mock child_process to prevent real spawnSync calls that would hang in tests
vi.mock('node:child_process', () => ({
spawn: vi.fn(),
spawnSync: vi.fn(() => ({ status: 0, stdout: '/usr/local/bin', stderr: '' })),
}));
const { checkForUpdates } = await import('./package-manager.js');
/** Helper: create a fetch mock that routes by URL pattern */
function createFetchMock() {
const handlers = new Map();
const mock = vi.fn((url, options) => {
const urlStr = typeof url === 'string' ? url : url.toString();
for (const [pattern, response] of handlers) {
if (urlStr.includes(pattern)) {
return Promise.resolve(response);
}
}
return Promise.reject(new Error(`Unexpected fetch call: ${urlStr}`));
});
mock.when = (pattern, response) => {
handlers.set(pattern, response);
return mock;
};
return mock;
}
describe('checkForUpdates', () => {
let fetchMock;
let originalFetch;
beforeEach(() => {
fetchMock = createFetchMock();
originalFetch = globalThis.fetch;
globalThis.fetch = fetchMock;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
// --- Scenario: API says update available, npm confirms ---
it('returns available=true when both API and npm confirm a newer version', async () => {
fetchMock
.when('api.openchamber.dev', {
ok: true,
json: async () => ({
latestVersion: '1.10.0',
updateAvailable: true,
releaseNotes: '## [1.10.0] - 2026-05-01\n\n- Great new feature',
}),
})
.when('registry.npmjs.org', {
ok: true,
json: async () => ({
'dist-tags': { latest: '1.10.0' },
}),
})
.when('raw.githubusercontent.com', {
ok: true,
text: async () => '## [1.10.0] - 2026-05-01\n\n- Great new feature',
});
const result = await checkForUpdates({ currentVersion: '1.9.10' });
expect(result.available).toBe(true);
expect(result.version).toBe('1.10.0');
expect(result.currentVersion).toBe('1.9.10');
});
// --- Scenario (THE FIX): API says update available, npm does NOT have it ---
it('returns available=false when API claims update but npm has same version', async () => {
fetchMock
.when('api.openchamber.dev', {
ok: true,
json: async () => ({
latestVersion: '1.10.0',
updateAvailable: true,
releaseNotes: '## [1.10.0] - 2026-05-01\n\n- Great new feature',
}),
})
.when('registry.npmjs.org', {
ok: true,
json: async () => ({
'dist-tags': { latest: '1.9.10' },
}),
});
const result = await checkForUpdates({ currentVersion: '1.9.10' });
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', {
ok: true,
json: async () => ({
latestVersion: '1.10.0',
updateAvailable: true,
releaseNotes: '## [1.10.0] - 2026-05-01\n\n- Great new feature',
}),
});
const result = await checkForUpdates({
appType: 'desktop-tauri',
currentVersion: '1.9.10',
});
expect(result.available).toBe(true);
expect(result.version).toBe('1.10.0');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('accepts electron desktop update claims without npm cross-checking', async () => {
fetchMock
.when('api.openchamber.dev', {
ok: true,
json: async () => ({
latestVersion: '1.10.0',
updateAvailable: true,
releaseNotes: '## [1.10.0] - 2026-05-01\n\n- Great new feature',
}),
});
const result = await checkForUpdates({
appType: 'desktop-electron',
currentVersion: '1.9.10',
});
expect(result.available).toBe(true);
expect(result.version).toBe('1.10.0');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('returns available=false when API claims update but npm is behind', async () => {
fetchMock
.when('api.openchamber.dev', {
ok: true,
json: async () => ({
latestVersion: '1.10.0',
updateAvailable: true,
releaseNotes: '## [1.10.0] - 2026-05-01\n\n- Great new feature',
}),
})
.when('registry.npmjs.org', {
ok: true,
json: async () => ({
'dist-tags': { latest: '1.9.9' },
}),
});
const result = await checkForUpdates({ currentVersion: '1.9.10' });
expect(result.available).toBe(false);
});
// --- Scenario: API says no update, npm agrees ---
it('returns available=false when API says no update and versions match', async () => {
fetchMock.when('api.openchamber.dev', {
ok: true,
json: async () => ({
latestVersion: '1.9.10',
updateAvailable: false,
}),
});
const result = await checkForUpdates({ currentVersion: '1.9.10' });
expect(result.available).toBe(false);
});
// --- Scenario: API unreachable, npm fallback ---
it('returns available=true from npm fallback when API is unreachable and npm has newer 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' },
}),
})
.when('raw.githubusercontent.com', {
ok: true,
text: async () => '## [1.10.0] - 2026-05-01\n\n- Great new feature',
});
const result = await checkForUpdates({ currentVersion: '1.9.10' });
expect(result.available).toBe(true);
expect(result.version).toBe('1.10.0');
});
it('returns available=false from npm fallback when API is unreachable and versions match', async () => {
fetchMock
.when('api.openchamber.dev', Promise.reject(new Error('Network error')))
.when('registry.npmjs.org', {
ok: true,
json: async () => ({
'dist-tags': { latest: '1.9.10' },
}),
});
const result = await checkForUpdates({ currentVersion: '1.9.10' });
expect(result.available).toBe(false);
});
// --- Scenario: API returns null (bad response), npm fallback ---
it('returns available=false when API returns non-ok status and versions match on npm', async () => {
fetchMock
.when('api.openchamber.dev', {
ok: false,
status: 500,
json: async () => ({}),
})
.when('registry.npmjs.org', {
ok: true,
json: async () => ({
'dist-tags': { latest: '1.9.10' },
}),
});
const result = await checkForUpdates({ currentVersion: '1.9.10' });
expect(result.available).toBe(false);
});
// --- Scenario: Both API and npm are unreachable ---
it('returns available=false when both sources are unreachable', async () => {
fetchMock
.when('api.openchamber.dev', Promise.reject(new Error('Network error')))
.when('registry.npmjs.org', Promise.reject(new Error('Registry unreachable')));
const result = await checkForUpdates({ currentVersion: '1.9.10' });
expect(result.available).toBe(false);
});
});