fix(ci): stabilize update-command tests under parallel runners

This commit is contained in:
Tam Nhu Tran
2026-04-13 11:56:30 -04:00
parent 72d8f36b5d
commit b148d1ba66
2 changed files with 89 additions and 99 deletions
+82 -80
View File
@@ -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', () => {