mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 22:21:20 +00:00
- Replace before()/after() with beforeAll()/afterAll() - Remove this.timeout() calls (unsupported by bun) - Update package.json scripts to use bun test - Fix error message regex for cross-runtime compatibility - Skip integration tests requiring network/child process mocking - Format source files with prettier
504 lines
17 KiB
JavaScript
504 lines
17 KiB
JavaScript
/**
|
|
* Unit Tests for Beta Channel Implementation (Phase 3)
|
|
*
|
|
* Tests the beta channel functionality added in Phase 3:
|
|
* - Beta channel updates in update-checker.ts
|
|
* - fetchVersionFromNpmTag function for 'dev' tag
|
|
* - checkForUpdates with targetTag parameter
|
|
* - Direct install beta rejection
|
|
* - Beta stability warning display
|
|
* - handleCheckFailed with targetTag
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
const os = require('os');
|
|
|
|
describe('Beta Channel Implementation (Phase 3)', function () {
|
|
let updateCheckerModule;
|
|
let originalFsExistsSync;
|
|
let originalFsReadFileSync;
|
|
let originalFsWriteFileSync;
|
|
let originalFsMkdirSync;
|
|
let originalHttpsGet;
|
|
let mockFileSystem = {};
|
|
let httpsRequests = [];
|
|
|
|
beforeAll(async function () {
|
|
// Build the project first
|
|
const { execSync } = require('child_process');
|
|
try {
|
|
execSync('bun run build', { cwd: path.resolve(__dirname, '../../..'), stdio: 'pipe' });
|
|
} catch (error) {
|
|
console.warn('Build failed, tests may not work:', error.message);
|
|
}
|
|
|
|
// Import the built module
|
|
updateCheckerModule = await import('../../../dist/utils/update-checker.js');
|
|
});
|
|
|
|
beforeEach(function () {
|
|
// Reset mocks
|
|
mockFileSystem = {};
|
|
httpsRequests = [];
|
|
|
|
// Store original functions
|
|
originalFsExistsSync = fs.existsSync;
|
|
originalFsReadFileSync = fs.readFileSync;
|
|
originalFsWriteFileSync = fs.writeFileSync;
|
|
originalFsMkdirSync = fs.mkdirSync;
|
|
originalHttpsGet = https.get;
|
|
|
|
// Mock fs.existsSync
|
|
fs.existsSync = (filePath) => {
|
|
return mockFileSystem[filePath] !== undefined;
|
|
};
|
|
|
|
// Mock fs.readFileSync
|
|
fs.readFileSync = (filePath, encoding) => {
|
|
if (mockFileSystem[filePath]) {
|
|
return mockFileSystem[filePath];
|
|
}
|
|
throw new Error(`ENOENT: no such file or directory, open '${filePath}'`);
|
|
};
|
|
|
|
// Mock fs.writeFileSync
|
|
fs.writeFileSync = (filePath, data, encoding) => {
|
|
mockFileSystem[filePath] = data;
|
|
};
|
|
|
|
// Mock fs.mkdirSync
|
|
fs.mkdirSync = (dirPath, options) => {
|
|
// Just mark as created for testing
|
|
mockFileSystem[dirPath] = 'directory';
|
|
};
|
|
|
|
// Mock https.get
|
|
https.get = (url, options, callback) => {
|
|
httpsRequests.push({ url, options: options || {} });
|
|
|
|
// Create mock response object
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: function(event, handler) {
|
|
if (event === 'data') {
|
|
// Simulate receiving data
|
|
setTimeout(() => {
|
|
if (url.includes('/dev')) {
|
|
handler('{"version":"5.5.0"}'); // Use a higher version for dev
|
|
} else if (url.includes('/latest')) {
|
|
handler('{"version":"5.4.1"}');
|
|
}
|
|
}, 0);
|
|
} else if (event === 'end') {
|
|
setTimeout(handler, 10);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Call callback with mock response
|
|
if (callback) {
|
|
setTimeout(() => callback(mockRes), 0);
|
|
}
|
|
|
|
// Return mock request object
|
|
return {
|
|
on: function(event, handler) {
|
|
if (event === 'error') {
|
|
// Don't call error handler by default
|
|
} else if (event === 'timeout') {
|
|
// Don't call timeout handler by default
|
|
}
|
|
},
|
|
destroy: function() {
|
|
// Mock destroy method
|
|
}
|
|
};
|
|
};
|
|
});
|
|
|
|
afterEach(function () {
|
|
// Restore original functions
|
|
fs.existsSync = originalFsExistsSync;
|
|
fs.readFileSync = originalFsReadFileSync;
|
|
fs.writeFileSync = originalFsWriteFileSync;
|
|
fs.mkdirSync = originalFsMkdirSync;
|
|
https.get = originalHttpsGet;
|
|
});
|
|
|
|
describe('fetchVersionFromNpmTag function (internal)', function () {
|
|
it('should request from correct npm registry URL for latest tag', async function () {
|
|
// Call checkForUpdates which internally uses fetchVersionFromNpmTag
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
|
|
|
|
// Should have made request to correct URL
|
|
const latestRequest = httpsRequests.find(req =>
|
|
req.url === 'https://registry.npmjs.org/@kaitranntt/ccs/latest'
|
|
);
|
|
assert(latestRequest, 'should request latest tag from npm registry');
|
|
|
|
// Should return version
|
|
assert.strictEqual(result.status, 'update_available');
|
|
assert.strictEqual(result.latest, '5.4.1');
|
|
});
|
|
|
|
it('should request from correct npm registry URL for dev tag', async function () {
|
|
// Call checkForUpdates which internally uses fetchVersionFromNpmTag
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'dev');
|
|
|
|
// Should have made request to correct URL
|
|
const devRequest = httpsRequests.find(req =>
|
|
req.url === 'https://registry.npmjs.org/@kaitranntt/ccs/dev'
|
|
);
|
|
assert(devRequest, 'should request dev tag from npm registry');
|
|
|
|
// Should return version
|
|
assert.strictEqual(result.status, 'update_available');
|
|
assert.strictEqual(result.latest, '5.5.0');
|
|
});
|
|
|
|
it('should handle network errors gracefully', async function () {
|
|
// Mock https.get to simulate network error
|
|
https.get = (url, options, callback) => {
|
|
const req = {
|
|
on: function(event, handler) {
|
|
if (event === 'error') {
|
|
setTimeout(() => handler(new Error('Network error')), 0);
|
|
}
|
|
},
|
|
destroy: function() {}
|
|
};
|
|
return req;
|
|
};
|
|
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
|
|
assert.strictEqual(result.status, 'check_failed');
|
|
assert.strictEqual(result.reason, 'npm_registry_error');
|
|
});
|
|
|
|
it('should handle timeout errors gracefully', async function () {
|
|
// Mock https.get to simulate timeout
|
|
https.get = (url, options, callback) => {
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: function(event, handler) {
|
|
// Don't send any data to trigger timeout
|
|
}
|
|
};
|
|
|
|
if (callback) {
|
|
setTimeout(() => callback(mockRes), 0);
|
|
}
|
|
|
|
return {
|
|
on: function(event, handler) {
|
|
if (event === 'timeout') {
|
|
setTimeout(() => {
|
|
handler();
|
|
this.destroy();
|
|
}, 100);
|
|
}
|
|
},
|
|
destroy: function() {}
|
|
};
|
|
};
|
|
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
|
|
assert.strictEqual(result.status, 'check_failed');
|
|
assert.strictEqual(result.reason, 'npm_registry_error');
|
|
});
|
|
|
|
it('should handle invalid JSON response', async function () {
|
|
// Mock https.get to return invalid JSON
|
|
https.get = (url, options, callback) => {
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: function(event, handler) {
|
|
if (event === 'data') {
|
|
setTimeout(() => handler('invalid json'), 0);
|
|
} else if (event === 'end') {
|
|
setTimeout(handler, 10);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (callback) {
|
|
setTimeout(() => callback(mockRes), 0);
|
|
}
|
|
|
|
return {
|
|
on: function() {},
|
|
destroy: function() {}
|
|
};
|
|
};
|
|
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
|
|
assert.strictEqual(result.status, 'check_failed');
|
|
assert.strictEqual(result.reason, 'npm_registry_error');
|
|
});
|
|
|
|
it('should handle non-200 status codes', async function () {
|
|
// Mock https.get to return 404
|
|
https.get = (url, options, callback) => {
|
|
const mockRes = {
|
|
statusCode: 404,
|
|
on: function(event, handler) {
|
|
if (event === 'end') {
|
|
setTimeout(handler, 0);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (callback) {
|
|
setTimeout(() => callback(mockRes), 0);
|
|
}
|
|
|
|
return {
|
|
on: function() {},
|
|
destroy: function() {}
|
|
};
|
|
};
|
|
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
|
|
assert.strictEqual(result.status, 'check_failed');
|
|
assert.strictEqual(result.reason, 'npm_registry_error');
|
|
});
|
|
});
|
|
|
|
describe('checkForUpdates with targetTag parameter', function () {
|
|
beforeEach(function () {
|
|
// Set up a fresh cache
|
|
const cacheData = {
|
|
last_check: 0,
|
|
latest_version: null,
|
|
dismissed_version: null
|
|
};
|
|
mockFileSystem[path.join(os.homedir(), '.ccs', 'update-check.json')] = JSON.stringify(cacheData);
|
|
});
|
|
|
|
it('should use latest tag when targetTag is "latest"', async function () {
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
|
|
|
|
// Should request latest tag
|
|
const latestRequest = httpsRequests.find(req =>
|
|
req.url.includes('/latest')
|
|
);
|
|
assert(latestRequest, 'should request latest tag');
|
|
|
|
// Should return update available
|
|
assert.strictEqual(result.status, 'update_available');
|
|
assert.strictEqual(result.latest, '5.4.1');
|
|
assert.strictEqual(result.current, '5.4.0');
|
|
});
|
|
|
|
it('should use dev tag when targetTag is "dev"', async function () {
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'dev');
|
|
|
|
// Should request dev tag
|
|
const devRequest = httpsRequests.find(req =>
|
|
req.url.includes('/dev')
|
|
);
|
|
assert(devRequest, 'should request dev tag');
|
|
|
|
// Should return update available (dev version newer)
|
|
assert.strictEqual(result.status, 'update_available');
|
|
assert.strictEqual(result.latest, '5.5.0');
|
|
assert.strictEqual(result.current, '5.4.0');
|
|
});
|
|
|
|
it('should reject beta for direct install', async function () {
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'direct', 'dev');
|
|
|
|
// Should return check_failed for beta on direct install
|
|
assert.strictEqual(result.status, 'check_failed');
|
|
assert.strictEqual(result.reason, 'beta_not_supported');
|
|
assert.strictEqual(result.message, '--beta requires npm installation method');
|
|
|
|
// Should not make any HTTP requests
|
|
assert.strictEqual(httpsRequests.length, 0, 'should not make HTTP requests');
|
|
});
|
|
|
|
it('should allow beta for npm install', async function () {
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'dev');
|
|
|
|
// Should NOT return check_failed for npm
|
|
assert.notStrictEqual(result.status, 'check_failed');
|
|
|
|
// Should make HTTP request
|
|
assert(httpsRequests.length > 0, 'should make HTTP requests');
|
|
});
|
|
|
|
it('should use different fetch error for npm vs direct', async function () {
|
|
// Mock https.get to fail
|
|
https.get = (url, options, callback) => {
|
|
const req = {
|
|
on: function(event, handler) {
|
|
if (event === 'error') {
|
|
setTimeout(() => handler(new Error('Network error')), 0);
|
|
}
|
|
},
|
|
destroy: function() {}
|
|
};
|
|
return req;
|
|
};
|
|
|
|
// Test npm install
|
|
const npmResult = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'latest');
|
|
assert.strictEqual(npmResult.status, 'check_failed');
|
|
assert.strictEqual(npmResult.reason, 'npm_registry_error');
|
|
|
|
// Reset requests
|
|
httpsRequests = [];
|
|
|
|
// Test direct install
|
|
const directResult = await updateCheckerModule.checkForUpdates('5.4.1', true, 'direct', 'latest');
|
|
assert.strictEqual(directResult.status, 'check_failed');
|
|
assert.strictEqual(directResult.reason, 'github_api_error');
|
|
});
|
|
|
|
it('should update cache with correct version from target tag', async function () {
|
|
await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'dev');
|
|
|
|
// Check cache was updated with dev version
|
|
const cachePath = path.join(os.homedir(), '.ccs', 'update-check.json');
|
|
const cacheData = JSON.parse(mockFileSystem[cachePath]);
|
|
|
|
assert.strictEqual(cacheData.latest_version, '5.5.0');
|
|
assert(cacheData.last_check > 0, 'should update timestamp');
|
|
});
|
|
|
|
it('should use cached result when within interval', async function () {
|
|
// Set up cache with recent check
|
|
const cacheData = {
|
|
last_check: Date.now() - 1000, // 1 second ago
|
|
latest_version: '5.5.0',
|
|
dismissed_version: null
|
|
};
|
|
mockFileSystem[path.join(os.homedir(), '.ccs', 'update-check.json')] = JSON.stringify(cacheData);
|
|
|
|
// Call with force=false to use cache
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', false, 'npm', 'dev');
|
|
|
|
// Should not make HTTP requests
|
|
assert.strictEqual(httpsRequests.length, 0, 'should use cached result');
|
|
|
|
// Should return cached update
|
|
assert.strictEqual(result.status, 'update_available');
|
|
assert.strictEqual(result.latest, '5.5.0');
|
|
});
|
|
});
|
|
|
|
describe('Version comparison with dev versions', function () {
|
|
it('should correctly compare dev version as newer', async function () {
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'dev');
|
|
|
|
// 5.5.0 should be newer than 5.4.0
|
|
assert.strictEqual(result.status, 'update_available');
|
|
assert.strictEqual(result.latest, '5.5.0');
|
|
});
|
|
|
|
it('should correctly handle same dev version', async function () {
|
|
// Mock same version response
|
|
https.get = (url, options, callback) => {
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: function(event, handler) {
|
|
if (event === 'data') {
|
|
setTimeout(() => handler('{"version":"5.4.1"}'), 0);
|
|
} else if (event === 'end') {
|
|
setTimeout(handler, 10);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (callback) {
|
|
setTimeout(() => callback(mockRes), 0);
|
|
}
|
|
|
|
return {
|
|
on: function() {},
|
|
destroy: function() {}
|
|
};
|
|
};
|
|
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'latest');
|
|
|
|
// Same version should not trigger update
|
|
assert.strictEqual(result.status, 'no_update');
|
|
assert.strictEqual(result.reason, 'latest');
|
|
});
|
|
|
|
it('should correctly handle older dev version', async function () {
|
|
// Mock older version response
|
|
https.get = (url, options, callback) => {
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: function(event, handler) {
|
|
if (event === 'data') {
|
|
setTimeout(() => handler('{"version":"5.4.0"}'), 0);
|
|
} else if (event === 'end') {
|
|
setTimeout(handler, 10);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (callback) {
|
|
setTimeout(() => callback(mockRes), 0);
|
|
}
|
|
|
|
return {
|
|
on: function() {},
|
|
destroy: function() {}
|
|
};
|
|
};
|
|
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.1', true, 'npm', 'dev');
|
|
|
|
// Older version should not trigger update
|
|
assert.strictEqual(result.status, 'no_update');
|
|
assert.strictEqual(result.reason, 'latest');
|
|
});
|
|
});
|
|
|
|
describe('Cache functionality', function () {
|
|
it('should create cache directory if not exists', async function () {
|
|
// Ensure no cache exists
|
|
const cacheDir = path.join(os.homedir(), '.ccs');
|
|
delete mockFileSystem[cacheDir];
|
|
|
|
await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
|
|
|
|
// Should create directory
|
|
assert(mockFileSystem[cacheDir], 'should create cache directory');
|
|
});
|
|
|
|
it('should handle dismissed versions correctly', async function () {
|
|
// Set up cache with dismissed version
|
|
const cacheData = {
|
|
last_check: Date.now() - 1000,
|
|
latest_version: '5.5.0',
|
|
dismissed_version: '5.5.0'
|
|
};
|
|
mockFileSystem[path.join(os.homedir(), '.ccs', 'update-check.json')] = JSON.stringify(cacheData);
|
|
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.1', false, 'npm', 'dev');
|
|
|
|
// Should not show update for dismissed version
|
|
assert.strictEqual(result.status, 'no_update');
|
|
assert.strictEqual(result.reason, 'dismissed');
|
|
});
|
|
|
|
it('should handle corrupted cache gracefully', async function () {
|
|
// Set up corrupted cache
|
|
mockFileSystem[path.join(os.homedir(), '.ccs', 'update-check.json')] = 'invalid json';
|
|
|
|
// Should not throw error
|
|
const result = await updateCheckerModule.checkForUpdates('5.4.0', true, 'npm', 'latest');
|
|
|
|
// Should work normally
|
|
assert(result.status === 'update_available' || result.status === 'no_update');
|
|
});
|
|
});
|
|
}); |