mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 22:17:14 +00:00
fix(ci): stabilize update-command tests under parallel runners
This commit is contained in:
@@ -31,6 +31,8 @@ type TargetTag = 'latest' | 'dev';
|
|||||||
export interface UpdateCommandDeps {
|
export interface UpdateCommandDeps {
|
||||||
initUI: typeof initUI;
|
initUI: typeof initUI;
|
||||||
getVersion: typeof getVersion;
|
getVersion: typeof getVersion;
|
||||||
|
log: typeof console.log;
|
||||||
|
exit: typeof process.exit;
|
||||||
detectCurrentInstall: typeof detectCurrentInstall;
|
detectCurrentInstall: typeof detectCurrentInstall;
|
||||||
buildPackageManagerEnv: typeof buildPackageManagerEnv;
|
buildPackageManagerEnv: typeof buildPackageManagerEnv;
|
||||||
formatManualUpdateCommand: typeof formatManualUpdateCommand;
|
formatManualUpdateCommand: typeof formatManualUpdateCommand;
|
||||||
@@ -58,6 +60,8 @@ async function loadCheckForUpdates(
|
|||||||
const defaultDeps: UpdateCommandDeps = {
|
const defaultDeps: UpdateCommandDeps = {
|
||||||
initUI,
|
initUI,
|
||||||
getVersion,
|
getVersion,
|
||||||
|
log: console.log,
|
||||||
|
exit: process.exit.bind(process) as typeof process.exit,
|
||||||
detectCurrentInstall,
|
detectCurrentInstall,
|
||||||
buildPackageManagerEnv,
|
buildPackageManagerEnv,
|
||||||
formatManualUpdateCommand,
|
formatManualUpdateCommand,
|
||||||
@@ -96,14 +100,14 @@ export async function handleUpdateCommand(
|
|||||||
const currentInstall = deps.detectCurrentInstall();
|
const currentInstall = deps.detectCurrentInstall();
|
||||||
const currentVersion = deps.getVersion();
|
const currentVersion = deps.getVersion();
|
||||||
|
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log(header('Checking for updates...'));
|
deps.log(header('Checking for updates...'));
|
||||||
console.log('');
|
deps.log('');
|
||||||
|
|
||||||
// Force reinstall - skip update check
|
// Force reinstall - skip update check
|
||||||
if (force) {
|
if (force) {
|
||||||
console.log(info(`Force reinstall from @${targetTag} channel...`));
|
deps.log(info(`Force reinstall from @${targetTag} channel...`));
|
||||||
console.log('');
|
deps.log('');
|
||||||
const expectedVersion = await resolveTargetVersion(currentVersion, targetTag, deps);
|
const expectedVersion = await resolveTargetVersion(currentVersion, targetTag, deps);
|
||||||
await performNpmUpdate(currentInstall, targetTag, true, expectedVersion, deps);
|
await performNpmUpdate(currentInstall, targetTag, true, expectedVersion, deps);
|
||||||
return;
|
return;
|
||||||
@@ -122,13 +126,13 @@ export async function handleUpdateCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateResult.status === 'no_update') {
|
if (updateResult.status === 'no_update') {
|
||||||
handleNoUpdate(updateResult.reason, currentVersion);
|
handleNoUpdate(updateResult.reason, currentVersion, deps);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update available
|
// Update available
|
||||||
console.log(warn(`Update available: ${updateResult.current} -> ${updateResult.latest}`));
|
deps.log(warn(`Update available: ${updateResult.current} -> ${updateResult.latest}`));
|
||||||
console.log('');
|
deps.log('');
|
||||||
|
|
||||||
// Check if this is a downgrade (e.g., stable to older dev)
|
// Check if this is a downgrade (e.g., stable to older dev)
|
||||||
const isDowngrade =
|
const isDowngrade =
|
||||||
@@ -138,7 +142,7 @@ export async function handleUpdateCommand(
|
|||||||
|
|
||||||
// This happens when stable user requests @dev but @dev base is older
|
// This happens when stable user requests @dev but @dev base is older
|
||||||
if (isDowngrade && beta) {
|
if (isDowngrade && beta) {
|
||||||
console.log(
|
deps.log(
|
||||||
warn(
|
warn(
|
||||||
'WARNING: Downgrading from ' +
|
'WARNING: Downgrading from ' +
|
||||||
(updateResult.current || 'unknown') +
|
(updateResult.current || 'unknown') +
|
||||||
@@ -146,16 +150,16 @@ export async function handleUpdateCommand(
|
|||||||
(updateResult.latest || 'unknown')
|
(updateResult.latest || 'unknown')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
console.log(warn('Dev channel may be behind stable.'));
|
deps.log(warn('Dev channel may be behind stable.'));
|
||||||
console.log('');
|
deps.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show beta warning
|
// Show beta warning
|
||||||
if (beta) {
|
if (beta) {
|
||||||
console.log(warn('Installing from @dev channel (unstable)'));
|
deps.log(warn('Installing from @dev channel (unstable)'));
|
||||||
console.log(warn('Not recommended for production use'));
|
deps.log(warn('Not recommended for production use'));
|
||||||
console.log(info('Use `ccs update` (without --beta) to return to stable'));
|
deps.log(info('Use `ccs update` (without --beta) to return to stable'));
|
||||||
console.log('');
|
deps.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
await performNpmUpdate(currentInstall, targetTag, false, updateResult.latest, deps);
|
await performNpmUpdate(currentInstall, targetTag, false, updateResult.latest, deps);
|
||||||
@@ -170,40 +174,44 @@ function handleCheckFailed(
|
|||||||
currentInstall: CurrentInstall = defaultDeps.detectCurrentInstall(),
|
currentInstall: CurrentInstall = defaultDeps.detectCurrentInstall(),
|
||||||
deps: UpdateCommandDeps = defaultDeps
|
deps: UpdateCommandDeps = defaultDeps
|
||||||
): void {
|
): void {
|
||||||
console.log(fail(message));
|
deps.log(fail(message));
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log(warn('Possible causes:'));
|
deps.log(warn('Possible causes:'));
|
||||||
console.log(' - Network connection issues');
|
deps.log(' - Network connection issues');
|
||||||
console.log(' - Firewall blocking requests');
|
deps.log(' - Firewall blocking requests');
|
||||||
console.log(' - GitHub/npm API temporarily unavailable');
|
deps.log(' - GitHub/npm API temporarily unavailable');
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log('Try again later or update manually:');
|
deps.log('Try again later or update manually:');
|
||||||
|
|
||||||
console.log(color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
deps.log(color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||||
console.log('');
|
deps.log('');
|
||||||
process.exit(1);
|
deps.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle no update available
|
* Handle no update available
|
||||||
*/
|
*/
|
||||||
function handleNoUpdate(reason: string | undefined, version: string): void {
|
function handleNoUpdate(
|
||||||
|
reason: string | undefined,
|
||||||
|
version: string,
|
||||||
|
deps: UpdateCommandDeps
|
||||||
|
): void {
|
||||||
let message = `You are already on the latest version (${version})`;
|
let message = `You are already on the latest version (${version})`;
|
||||||
|
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'dismissed':
|
case 'dismissed':
|
||||||
message = `Update dismissed. You are on version ${version}`;
|
message = `Update dismissed. You are on version ${version}`;
|
||||||
console.log(warn(message));
|
deps.log(warn(message));
|
||||||
break;
|
break;
|
||||||
case 'cached':
|
case 'cached':
|
||||||
message = `No updates available (cached result). You are on version ${version}`;
|
message = `No updates available (cached result). You are on version ${version}`;
|
||||||
console.log(info(message));
|
deps.log(info(message));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(ok(message));
|
deps.log(ok(message));
|
||||||
}
|
}
|
||||||
console.log('');
|
deps.log('');
|
||||||
process.exit(0);
|
deps.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,15 +228,13 @@ async function verifyCurrentInstallVersion(
|
|||||||
const nextState = deps.readInstalledPackageState(currentInstall);
|
const nextState = deps.readInstalledPackageState(currentInstall);
|
||||||
const installedVersion = nextState.version;
|
const installedVersion = nextState.version;
|
||||||
if (!installedVersion) {
|
if (!installedVersion) {
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log(
|
deps.log(fail('Update finished, but CCS could not verify the current installation version.'));
|
||||||
fail('Update finished, but CCS could not verify the current installation version.')
|
deps.log('');
|
||||||
);
|
deps.log('Current install remains ambiguous. Re-run manually:');
|
||||||
console.log('');
|
deps.log(color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||||
console.log('Current install remains ambiguous. Re-run manually:');
|
deps.log('');
|
||||||
console.log(color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
deps.exit(1);
|
||||||
console.log('');
|
|
||||||
process.exit(1);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,26 +265,24 @@ async function verifyCurrentInstallVersion(
|
|||||||
|
|
||||||
const comparison = deps.compareVersionsWithPrerelease(installedVersion, expectedVersion);
|
const comparison = deps.compareVersionsWithPrerelease(installedVersion, expectedVersion);
|
||||||
if (comparison < 0 || installedVersion === previousState?.version) {
|
if (comparison < 0 || installedVersion === previousState?.version) {
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log(
|
deps.log(
|
||||||
fail(
|
fail(
|
||||||
`Update completed outside the current installation. Current binary still reports ${installedVersion}; expected ${expectedVersion}.`
|
`Update completed outside the current installation. Current binary still reports ${installedVersion}; expected ${expectedVersion}.`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (previousState?.version && previousState.version === installedVersion) {
|
if (previousState?.version && previousState.version === installedVersion) {
|
||||||
console.log(
|
deps.log(
|
||||||
warn(
|
warn(
|
||||||
`The current install path did not change from ${previousState.version}; another package manager likely updated a different copy of CCS.`
|
`The current install path did not change from ${previousState.version}; another package manager likely updated a different copy of CCS.`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log('Re-run manually against the current install:');
|
deps.log('Re-run manually against the current install:');
|
||||||
console.log(
|
deps.log(color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||||
color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command')
|
deps.log('');
|
||||||
);
|
deps.exit(1);
|
||||||
console.log('');
|
|
||||||
process.exit(1);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,8 +293,8 @@ async function verifyCurrentInstallVersion(
|
|||||||
installedVersion === previousState.version &&
|
installedVersion === previousState.version &&
|
||||||
!installChanged
|
!installChanged
|
||||||
) {
|
) {
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log(
|
deps.log(
|
||||||
warn(
|
warn(
|
||||||
`Reinstall completed, but CCS could not prove that the current installation changed from ${previousState.version}. Verify the current binary manually if this reinstall was meant to repair a same-version install.`
|
`Reinstall completed, but CCS could not prove that the current installation changed from ${previousState.version}. Verify the current binary manually if this reinstall was meant to repair a same-version install.`
|
||||||
)
|
)
|
||||||
@@ -391,8 +395,8 @@ async function performNpmUpdate(
|
|||||||
cacheArgs = ['cache', 'clean', '--force'];
|
cacheArgs = ['cache', 'clean', '--force'];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(info(`${isReinstall ? 'Reinstalling' : 'Updating'} via ${packageManager}...`));
|
deps.log(info(`${isReinstall ? 'Reinstalling' : 'Updating'} via ${packageManager}...`));
|
||||||
console.log('');
|
deps.log('');
|
||||||
|
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
@@ -406,17 +410,17 @@ async function performNpmUpdate(
|
|||||||
? 'Pre-removal failed, proceeding anyway...'
|
? 'Pre-removal failed, proceeding anyway...'
|
||||||
: 'Cache clearing failed, proceeding anyway...';
|
: 'Cache clearing failed, proceeding anyway...';
|
||||||
|
|
||||||
console.log(info(stepMessage));
|
deps.log(info(stepMessage));
|
||||||
try {
|
try {
|
||||||
const cacheCode = await runChildProcess(deps, cacheCommand, cacheArgs, {
|
const cacheCode = await runChildProcess(deps, cacheCommand, cacheArgs, {
|
||||||
isWindows,
|
isWindows,
|
||||||
env: childEnv,
|
env: childEnv,
|
||||||
});
|
});
|
||||||
if (cacheCode !== 0) {
|
if (cacheCode !== 0) {
|
||||||
console.log(warn(failMessage));
|
deps.log(warn(failMessage));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
console.log(warn(failMessage));
|
deps.log(warn(failMessage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,31 +442,29 @@ async function performNpmUpdate(
|
|||||||
deps
|
deps
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log(ok(`${isReinstall ? 'Reinstall' : 'Update'} successful!`));
|
deps.log(ok(`${isReinstall ? 'Reinstall' : 'Update'} successful!`));
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log(`Run ${color('ccs --version', 'command')} to verify`);
|
deps.log(`Run ${color('ccs --version', 'command')} to verify`);
|
||||||
console.log(info(`Tip: Use ${color('ccs config', 'command')} for web-based configuration`));
|
deps.log(info(`Tip: Use ${color('ccs config', 'command')} for web-based configuration`));
|
||||||
console.log('');
|
deps.log('');
|
||||||
} else {
|
} else {
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log(fail(`${isReinstall ? 'Reinstall' : 'Update'} failed`));
|
deps.log(fail(`${isReinstall ? 'Reinstall' : 'Update'} failed`));
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log('Try manually:');
|
deps.log('Try manually:');
|
||||||
console.log(
|
deps.log(color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||||
color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command')
|
deps.log('');
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(exitCode || 0);
|
deps.exit(exitCode || 0);
|
||||||
} catch {
|
} catch {
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log(fail(`Failed to run ${packageManager} ${isReinstall ? 'reinstall' : 'update'}`));
|
deps.log(fail(`Failed to run ${packageManager} ${isReinstall ? 'reinstall' : 'update'}`));
|
||||||
console.log('');
|
deps.log('');
|
||||||
console.log('Try manually:');
|
deps.log('Try manually:');
|
||||||
console.log(color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
deps.log(color(` ${deps.formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||||
console.log('');
|
deps.log('');
|
||||||
process.exit(1);
|
deps.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||||
import { handleUpdateCommand, type UpdateCommandDeps } from '../../../src/commands/update-command';
|
import { handleUpdateCommand, type UpdateCommandDeps } from '../../../src/commands/update-command';
|
||||||
import type { UpdateResult } from '../../../src/utils/update-checker';
|
import type { UpdateResult } from '../../../src/utils/update-checker';
|
||||||
|
|
||||||
let logLines: string[] = [];
|
let logLines: string[] = [];
|
||||||
let spawnCalls: Array<{ command: string; args: string[]; env?: NodeJS.ProcessEnv }> = [];
|
let spawnCalls: Array<{ command: string; args: string[]; env?: NodeJS.ProcessEnv }> = [];
|
||||||
let exitCodes: number[] = [];
|
let exitCodes: number[] = [];
|
||||||
let originalConsoleLog: typeof console.log;
|
|
||||||
let originalProcessExit: typeof process.exit;
|
|
||||||
|
|
||||||
type InstalledState = {
|
type InstalledState = {
|
||||||
version: string | null;
|
version: string | null;
|
||||||
@@ -39,6 +37,12 @@ function createDeps(overrides: Partial<UpdateCommandDeps> = {}): UpdateCommandDe
|
|||||||
return {
|
return {
|
||||||
initUI: async () => {},
|
initUI: async () => {},
|
||||||
getVersion: () => '7.67.0-dev.5',
|
getVersion: () => '7.67.0-dev.5',
|
||||||
|
log: (...args: unknown[]) => {
|
||||||
|
logLines.push(args.map(String).join(' '));
|
||||||
|
},
|
||||||
|
exit: ((code?: number) => {
|
||||||
|
exitCodes.push(code ?? 0);
|
||||||
|
}) as typeof process.exit,
|
||||||
detectCurrentInstall: () => currentInstallOverride,
|
detectCurrentInstall: () => currentInstallOverride,
|
||||||
buildPackageManagerEnv: () => {
|
buildPackageManagerEnv: () => {
|
||||||
if (currentInstallOverride.manager === 'npm') {
|
if (currentInstallOverride.manager === 'npm') {
|
||||||
@@ -110,22 +114,6 @@ beforeEach(() => {
|
|||||||
latest: '7.67.0-dev.9',
|
latest: '7.67.0-dev.9',
|
||||||
};
|
};
|
||||||
currentInstallOverride = installDescriptor();
|
currentInstallOverride = installDescriptor();
|
||||||
|
|
||||||
originalConsoleLog = console.log;
|
|
||||||
originalProcessExit = process.exit;
|
|
||||||
|
|
||||||
console.log = (...args: unknown[]) => {
|
|
||||||
logLines.push(args.map(String).join(' '));
|
|
||||||
};
|
|
||||||
|
|
||||||
process.exit = ((code?: number) => {
|
|
||||||
exitCodes.push(code ?? 0);
|
|
||||||
}) as typeof process.exit;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
console.log = originalConsoleLog;
|
|
||||||
process.exit = originalProcessExit;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('update-command current install handling', () => {
|
describe('update-command current install handling', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user