fix(update): refresh target before foreground prompts (#405)

This commit is contained in:
liruifengv 2026-06-04 13:10:20 +08:00 committed by GitHub
parent 7a4c80eae1
commit 07e2e0f094
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 114 additions and 9 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Refresh the update target before showing foreground update prompts so the displayed version matches the install.

View file

@ -42,6 +42,7 @@ export interface RunUpdatePreflightOptions {
const AUTO_INSTALL_FAILURE_PROMPT_THRESHOLD = 2;
const AUTO_INSTALL_ACTIVE_TTL_MS = 6 * 60 * 60 * 1000;
const USER_VISIBLE_UPDATE_REFRESH_TIMEOUT_MS = 1_000;
type UpdateLogger = Pick<Logger, 'info' | 'warn'>;
@ -191,6 +192,30 @@ function refreshAndMaybeInstallInBackground(
})().catch(() => {});
}
async function refreshUserVisibleUpdateTarget(
currentVersion: string,
fallbackTarget: UpdateTarget,
): Promise<UpdateTarget | null> {
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
const refresh = refreshUpdateCache()
.then((refreshed) => selectUpdateTarget(currentVersion, refreshed.latest))
.catch(() => fallbackTarget);
const fallback = new Promise<UpdateTarget>((resolve) => {
timeout = setTimeout(() => {
resolve(fallbackTarget);
}, USER_VISIBLE_UPDATE_REFRESH_TIMEOUT_MS);
});
return await Promise.race([refresh, fallback]);
} catch {
return fallbackTarget;
} finally {
if (timeout !== undefined) {
clearTimeout(timeout);
}
}
}
function nowIso(): string {
return new Date().toISOString();
}
@ -536,16 +561,17 @@ export async function runUpdatePreflight(
return 'continue';
}
refreshInBackground();
const source: InstallSource =
!isInteractive
? 'unsupported'
: await detectInstallSource().catch(() => 'unsupported' as const);
const decision = decideUpdateAction(target, isInteractive, source, platform);
if (decision === 'none') return 'continue';
if (decision === 'none') {
refreshInBackground();
return 'continue';
}
const installCommand = installCommandFor(source, target.version, platform);
if (
await tryStartAutomaticBackgroundInstall(
installState,
@ -557,26 +583,49 @@ export async function runUpdatePreflight(
logger,
)
) {
refreshInBackground();
return 'continue';
}
trackUpdatePrompted(options.track, currentVersion, target, source, decision);
const userVisibleTarget = await refreshUserVisibleUpdateTarget(currentVersion, target);
if (userVisibleTarget === null) return 'continue';
if (
await tryStartAutomaticBackgroundInstall(
installState,
currentVersion,
userVisibleTarget,
source,
platform,
options.track,
logger,
)
) {
return 'continue';
}
const installCommand = installCommandFor(source, userVisibleTarget.version, platform);
trackUpdatePrompted(options.track, currentVersion, userVisibleTarget, source, decision);
if (decision === 'manual-command') {
stdout.write(renderManualUpdateMessage(currentVersion, target, source, installCommand));
stdout.write(renderManualUpdateMessage(
currentVersion,
userVisibleTarget,
source,
installCommand,
));
return 'continue';
}
const choice = await promptInstall(currentVersion, target, source, installCommand);
const choice = await promptInstall(currentVersion, userVisibleTarget, source, installCommand);
if (choice === 'skip') return 'continue';
try {
await installUpdate(source, target.version, platform);
stdout.write(renderInstallSuccessMessage(target));
await installUpdate(source, userVisibleTarget.version, platform);
stdout.write(renderInstallSuccessMessage(userVisibleTarget));
return 'exit';
} catch (error) {
stderr.write(
`warning: failed to install ${NPM_PACKAGE_NAME}@${target.version}: ` +
`warning: failed to install ${NPM_PACKAGE_NAME}@${userVisibleTarget.version}: ` +
`${formatErrorMessage(error)}\n`,
);
return 'continue';

View file

@ -273,6 +273,57 @@ describe('runUpdatePreflight', () => {
expect(stdout.join('')).toContain('Updated @moonshot-ai/kimi-code to 0.5.0');
});
it('refreshes a stale cached target before showing the foreground install prompt', async () => {
disableAutoInstall();
mocks.readUpdateCache.mockResolvedValue(cacheWith('0.6.0'));
mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.7.0'));
mocks.detectInstallSource.mockResolvedValue('npm-global');
mocks.promptForInstallChoice.mockResolvedValue('install');
mockSpawnExit(0);
const { stdout, options } = captureOutput();
await expect(runUpdatePreflight('0.5.0', options)).resolves.toBe('exit');
expect(refreshUpdateCache).toHaveBeenCalledTimes(1);
expect(mocks.promptForInstallChoice).toHaveBeenCalledWith(
expect.objectContaining({
target: { version: '0.7.0' },
installCommand: 'npm install -g @moonshot-ai/kimi-code@0.7.0',
}),
);
expect(mocks.spawn).toHaveBeenCalledWith(
expect.stringMatching(/^npm(\.cmd)?$/),
['install', '-g', '@moonshot-ai/kimi-code@0.7.0'],
{ stdio: 'inherit' },
);
expect(stdout.join('')).toContain('Updated @moonshot-ai/kimi-code to 0.7.0');
});
it('falls back to the cached foreground prompt target when the refresh hangs', async () => {
vi.useFakeTimers();
try {
disableAutoInstall();
mocks.readUpdateCache.mockResolvedValue(cacheWith('0.6.0'));
mocks.refreshUpdateCache.mockReturnValue(new Promise(() => {}));
mocks.detectInstallSource.mockResolvedValue('npm-global');
mocks.promptForInstallChoice.mockResolvedValue('skip');
const { options } = captureOutput();
const result = runUpdatePreflight('0.5.0', options);
await vi.advanceTimersByTimeAsync(1_000);
await expect(result).resolves.toBe('continue');
expect(mocks.promptForInstallChoice).toHaveBeenCalledWith(
expect.objectContaining({
target: { version: '0.6.0' },
installCommand: 'npm install -g @moonshot-ai/kimi-code@0.6.0',
}),
);
} finally {
vi.useRealTimers();
}
});
it('pnpm-global: spawns pnpm add -g', async () => {
disableAutoInstall();
mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0'));