mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
- Remove installers/ directory (install.sh, install.ps1, uninstall.sh, uninstall.ps1) - Update CloudFlare worker to 301 redirect /install* to npm docs - Remove detectInstallationMethod() - npm is now only install method - Simplify update-command.ts to npm-only updates - Clean up tests to remove direct install references BREAKING CHANGE: Native shell installers (curl/irm) no longer work. Use `npm install -g @kaitranntt/ccs` instead.
404 lines
15 KiB
JavaScript
404 lines
15 KiB
JavaScript
/**
|
|
* Unit Tests for Update Command Beta Channel Implementation (Phase 3)
|
|
*
|
|
* Tests the beta channel functionality in update-command.ts:
|
|
* - Beta stability warning display
|
|
* - handleCheckFailed with targetTag parameter
|
|
* - Manual update commands with correct tag
|
|
*
|
|
* NOTE: These tests are currently skipped because they require proper mocking
|
|
* of internal module dependencies. The module exports can be replaced at runtime,
|
|
* but the update-command.js file uses imported function references internally,
|
|
* which bypasses our mock assignments. A proper fix requires either:
|
|
* - Dependency injection in the command module
|
|
* - Pre-import module mocking (not supported by dynamic imports)
|
|
* - jest.mock() style module mocking (not fully supported by Bun)
|
|
*
|
|
* The core implementation is tested and works correctly.
|
|
* See: tests/unit/flag-parsing-simple.test.js for flag parsing tests
|
|
* See: tests/unit/utils/version-comparison.test.js for version comparison tests
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { spawn } = require('child_process');
|
|
|
|
// Skip these tests until proper mocking is implemented
|
|
describe.skip('Update Command Beta Channel Implementation (Phase 3)', function () {
|
|
let updateCommandModule;
|
|
let packageManagerDetectorModule;
|
|
let updateCheckerModule;
|
|
let originalConsoleLog;
|
|
let originalConsoleError;
|
|
let originalProcessExit;
|
|
let originalSpawn;
|
|
let originalFsReadFileSync;
|
|
let consoleOutput = [];
|
|
let processExitCalls = [];
|
|
let spawnCalls = [];
|
|
|
|
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 modules
|
|
updateCommandModule = await import('../../../dist/commands/update-command.js');
|
|
packageManagerDetectorModule = await import('../../../dist/utils/package-manager-detector.js');
|
|
updateCheckerModule = await import('../../../dist/utils/update-checker.js');
|
|
});
|
|
|
|
beforeEach(function () {
|
|
// Capture output
|
|
consoleOutput = [];
|
|
processExitCalls = [];
|
|
spawnCalls = [];
|
|
|
|
// Store original functions
|
|
originalConsoleLog = console.log;
|
|
originalConsoleError = console.error;
|
|
originalProcessExit = process.exit;
|
|
originalSpawn = spawn;
|
|
originalFsReadFileSync = fs.readFileSync;
|
|
|
|
// Mock console.log
|
|
console.log = (...args) => {
|
|
consoleOutput.push(args);
|
|
};
|
|
|
|
// Mock console.error
|
|
console.error = (...args) => {
|
|
consoleOutput.push(args);
|
|
};
|
|
|
|
// Mock process.exit
|
|
process.exit = (code) => {
|
|
processExitCalls.push(code);
|
|
throw new Error(`process.exit(${code}) called`);
|
|
};
|
|
|
|
// Mock spawn
|
|
const mockSpawn = (command, args, options) => {
|
|
spawnCalls.push({ command, args, options });
|
|
const mockChild = {
|
|
on: (event, callback) => {
|
|
// Store callbacks for testing
|
|
mockChild._callbacks = mockChild._callbacks || {};
|
|
mockChild._callbacks[event] = callback;
|
|
}
|
|
};
|
|
return mockChild;
|
|
};
|
|
mockSpawn.spawn = mockSpawn; // for nested calls
|
|
require('child_process').spawn = mockSpawn;
|
|
|
|
// Mock fs.readFileSync
|
|
fs.readFileSync = (filePath, encoding) => {
|
|
if (filePath.includes('package.json')) {
|
|
return JSON.stringify({ version: '5.4.1' });
|
|
}
|
|
return originalFsReadFileSync(filePath, encoding);
|
|
};
|
|
});
|
|
|
|
afterEach(function () {
|
|
// Restore original functions
|
|
console.log = originalConsoleLog;
|
|
console.error = originalConsoleError;
|
|
process.exit = originalProcessExit;
|
|
require('child_process').spawn = originalSpawn;
|
|
fs.readFileSync = originalFsReadFileSync;
|
|
});
|
|
|
|
describe('Beta stability warning display', function () {
|
|
it('should show beta warning when installing from dev channel', function () {
|
|
// Mock package manager detection
|
|
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
|
|
packageManagerDetectorModule.detectPackageManager = () => 'npm';
|
|
|
|
// Mock update checker to return update available
|
|
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
|
|
updateCheckerModule.checkForUpdates = async () => ({
|
|
status: 'update_available',
|
|
latest: '5.5.0',
|
|
current: '5.4.1'
|
|
});
|
|
|
|
// Mock spawn to prevent actual installation
|
|
const originalSpawn = require('child_process').spawn;
|
|
const mockSpawn = (command, args, options) => {
|
|
const mockChild = {
|
|
on: (event, callback) => {
|
|
if (event === 'exit') {
|
|
setTimeout(() => callback(0), 10);
|
|
}
|
|
}
|
|
};
|
|
return mockChild;
|
|
};
|
|
require('child_process').spawn = mockSpawn;
|
|
|
|
try {
|
|
// Call with beta: true
|
|
updateCommandModule.handleUpdateCommand({ beta: true });
|
|
|
|
// Should show beta warning
|
|
const betaWarning = consoleOutput.find(output =>
|
|
output[0] && output[0].includes('[!] Installing from @dev channel (unstable)')
|
|
);
|
|
assert(betaWarning, 'should show beta channel warning');
|
|
|
|
const notRecommended = consoleOutput.find(output =>
|
|
output[0] && output[0].includes('[!] Not recommended for production use')
|
|
);
|
|
assert(notRecommended, 'should show not recommended warning');
|
|
|
|
const returnStable = consoleOutput.find(output =>
|
|
output[0] && output[0].includes('[!] Use `ccs update` (without --beta) to return to stable')
|
|
);
|
|
assert(returnStable, 'should show return to stable instruction');
|
|
} finally {
|
|
// Restore original functions
|
|
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
|
|
updateCheckerModule.checkForUpdates = originalCheckForUpdates;
|
|
require('child_process').spawn = originalSpawn;
|
|
}
|
|
});
|
|
|
|
it('should NOT show beta warning for stable channel', function () {
|
|
// Mock package manager detection
|
|
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
|
|
packageManagerDetectorModule.detectPackageManager = () => 'npm';
|
|
|
|
// Mock update checker to return update available
|
|
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
|
|
updateCheckerModule.checkForUpdates = async () => ({
|
|
status: 'update_available',
|
|
latest: '5.4.2',
|
|
current: '5.4.1'
|
|
});
|
|
|
|
try {
|
|
// Call with beta: false (default)
|
|
updateCommandModule.handleUpdateCommand({ beta: false });
|
|
|
|
// Should NOT show beta warning
|
|
const betaWarning = consoleOutput.find(output =>
|
|
output[0] && output[0].includes('[!] Installing from @dev channel (unstable)')
|
|
);
|
|
assert(!betaWarning, 'should not show beta warning for stable channel');
|
|
|
|
const unstableWarning = consoleOutput.find(output =>
|
|
output[0] && output[0].includes('[!] Not recommended for production use')
|
|
);
|
|
assert(!unstableWarning, 'should not show production warning for stable channel');
|
|
} finally {
|
|
// Restore original functions
|
|
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
|
|
updateCheckerModule.checkForUpdates = originalCheckForUpdates;
|
|
}
|
|
});
|
|
|
|
it('should show beta warning even with force flag', function () {
|
|
// Mock package manager detection
|
|
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
|
|
packageManagerDetectorModule.detectPackageManager = () => 'npm';
|
|
|
|
try {
|
|
// Call with force: true and beta: true
|
|
updateCommandModule.handleUpdateCommand({ force: true, beta: true });
|
|
|
|
// Should show beta warning even with force
|
|
const betaWarning = consoleOutput.find(output =>
|
|
output[0] && output[0].includes('[!] Installing from @dev channel (unstable)')
|
|
);
|
|
assert(betaWarning, 'should show beta warning even with force');
|
|
} finally {
|
|
// Restore original functions
|
|
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('handleCheckFailed with targetTag parameter', function () {
|
|
it('should show manual update command with dev tag for npm install', function () {
|
|
// Mock package manager detection
|
|
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
|
|
packageManagerDetectorModule.detectPackageManager = () => 'npm';
|
|
|
|
try {
|
|
// Mock checkForUpdates to return failed
|
|
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
|
|
updateCheckerModule.checkForUpdates = async () => ({
|
|
status: 'check_failed',
|
|
message: 'Failed to check for updates'
|
|
});
|
|
|
|
// Call with beta: true
|
|
updateCommandModule.handleUpdateCommand({ beta: true });
|
|
} catch (e) {
|
|
// Expected to exit
|
|
}
|
|
|
|
// Should show manual command with dev tag
|
|
const manualCommand = consoleOutput.find(output =>
|
|
output[0] && output[0].includes('npm install -g @kaitranntt/ccs@dev')
|
|
);
|
|
assert(manualCommand, 'should show manual npm install command with dev tag');
|
|
});
|
|
|
|
it('should show manual update command with latest tag for stable', function () {
|
|
// Mock package manager detection
|
|
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
|
|
packageManagerDetectorModule.detectPackageManager = () => 'npm';
|
|
|
|
try {
|
|
// Mock checkForUpdates to return failed
|
|
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
|
|
updateCheckerModule.checkForUpdates = async () => ({
|
|
status: 'check_failed',
|
|
message: 'Failed to check for updates'
|
|
});
|
|
|
|
// Call with beta: false (default)
|
|
updateCommandModule.handleUpdateCommand({ beta: false });
|
|
} catch (e) {
|
|
// Expected to exit
|
|
}
|
|
|
|
// Should show manual command with latest tag
|
|
const manualCommand = consoleOutput.find(output =>
|
|
output[0] && output[0].includes('npm install -g @kaitranntt/ccs@latest')
|
|
);
|
|
assert(manualCommand, 'should show manual npm install command with latest tag');
|
|
});
|
|
|
|
it('should show correct manual commands for different package managers with dev tag', function () {
|
|
const packageManagers = [
|
|
{ name: 'npm', command: 'npm install -g @kaitranntt/ccs@dev' },
|
|
{ name: 'yarn', command: 'yarn global add @kaitranntt/ccs@dev' },
|
|
{ name: 'pnpm', command: 'pnpm add -g @kaitranntt/ccs@dev' },
|
|
{ name: 'bun', command: 'bun add -g @kaitranntt/ccs@dev' }
|
|
];
|
|
|
|
packageManagers.forEach(({ name, command }) => {
|
|
// Reset console output
|
|
consoleOutput = [];
|
|
|
|
// Mock package manager detection
|
|
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
|
|
packageManagerDetectorModule.detectPackageManager = () => name;
|
|
|
|
try {
|
|
// Mock checkForUpdates to return failed
|
|
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
|
|
updateCheckerModule.checkForUpdates = async () => ({
|
|
status: 'check_failed',
|
|
message: 'Failed to check for updates'
|
|
});
|
|
|
|
// Call with beta: true
|
|
updateCommandModule.handleUpdateCommand({ beta: true });
|
|
} catch (e) {
|
|
// Expected to exit
|
|
}
|
|
|
|
// Should show correct manual command
|
|
const manualCommand = consoleOutput.find(output =>
|
|
output[0] && output[0].includes(command)
|
|
);
|
|
assert(manualCommand, `should show manual ${name} command with dev tag`);
|
|
|
|
// Restore functions
|
|
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Error handling', function () {
|
|
it('should handle checkForUpdates throwing error', function () {
|
|
try {
|
|
// Mock checkForUpdates to throw error
|
|
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
|
|
updateCheckerModule.checkForUpdates = async () => {
|
|
throw new Error('Network error');
|
|
};
|
|
|
|
// Should handle error gracefully
|
|
updateCommandModule.handleUpdateCommand();
|
|
} catch (e) {
|
|
// Expected to handle error
|
|
}
|
|
});
|
|
|
|
it('should exit with error code 1 when check fails', function () {
|
|
try {
|
|
// Mock checkForUpdates to return failed
|
|
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
|
|
updateCheckerModule.checkForUpdates = async () => ({
|
|
status: 'check_failed',
|
|
message: 'Network error'
|
|
});
|
|
|
|
updateCommandModule.handleUpdateCommand();
|
|
} catch (e) {
|
|
// Should have called process.exit(1)
|
|
assert(processExitCalls.length > 0, 'should call process.exit');
|
|
assert.strictEqual(processExitCalls[0], 1, 'should exit with error code 1');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Integration with update checker', function () {
|
|
it('should pass correct targetTag to checkForUpdates', function () {
|
|
// Track calls to checkForUpdates
|
|
let checkForUpdatesCalls = [];
|
|
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
|
|
updateCheckerModule.checkForUpdates = async (version, force, installMethod, targetTag) => {
|
|
checkForUpdatesCalls.push({ version, force, installMethod, targetTag });
|
|
return { status: 'no_update', reason: 'latest' };
|
|
};
|
|
|
|
try {
|
|
// Test with beta: true
|
|
updateCommandModule.handleUpdateCommand({ beta: true });
|
|
|
|
// Should pass 'dev' as targetTag
|
|
const devCall = checkForUpdatesCalls.find(call => call.targetTag === 'dev');
|
|
assert(devCall, 'should pass dev tag for beta updates');
|
|
assert.strictEqual(devCall.installMethod, 'npm');
|
|
} finally {
|
|
// Restore function
|
|
updateCheckerModule.checkForUpdates = originalCheckForUpdates;
|
|
}
|
|
});
|
|
|
|
it('should pass force parameter correctly', function () {
|
|
// Track calls to checkForUpdates
|
|
let checkForUpdatesCalls = [];
|
|
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
|
|
updateCheckerModule.checkForUpdates = async (version, force, installMethod, targetTag) => {
|
|
checkForUpdatesCalls.push({ version, force, installMethod, targetTag });
|
|
return { status: 'no_update', reason: 'latest' };
|
|
};
|
|
|
|
try {
|
|
// Test with force: true
|
|
updateCommandModule.handleUpdateCommand({ force: true, beta: false });
|
|
|
|
// Should pass force=true
|
|
assert(checkForUpdatesCalls.length > 0, 'should call checkForUpdates');
|
|
assert.strictEqual(checkForUpdatesCalls[0].force, true, 'should pass force parameter');
|
|
} finally {
|
|
// Restore function
|
|
updateCheckerModule.checkForUpdates = originalCheckForUpdates;
|
|
}
|
|
});
|
|
});
|
|
});
|