diff --git a/.changeset/interactive-update-check-timeout.md b/.changeset/interactive-update-check-timeout.md new file mode 100644 index 00000000000..947e2533092 --- /dev/null +++ b/.changeset/interactive-update-check-timeout.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Increase the request timeout for `kimi update`. diff --git a/apps/kimi-code/src/cli/sub/upgrade.ts b/apps/kimi-code/src/cli/sub/upgrade.ts index c5471064571..861d6ddf84e 100644 --- a/apps/kimi-code/src/cli/sub/upgrade.ts +++ b/apps/kimi-code/src/cli/sub/upgrade.ts @@ -1,6 +1,8 @@ import { log, type Logger } from '@moonshot-ai/kimi-code-sdk'; import { track as trackTelemetry, type TelemetryProperties } from '@moonshot-ai/kimi-telemetry'; +import { INTERACTIVE_UPDATE_CHECK_TIMEOUT_MS } from '#/constant/app'; + import { refreshUpdateCache } from '#/cli/update/refresh'; import { selectUpdateTarget } from '#/cli/update/select'; import { detectInstallSource } from '#/cli/update/source'; @@ -174,7 +176,9 @@ export async function handleUpgrade( function createDefaultUpgradeDeps(overrides: Partial): UpgradeDeps { return { - refreshUpdateCache: overrides.refreshUpdateCache ?? (() => refreshUpdateCache()), + refreshUpdateCache: + overrides.refreshUpdateCache ?? + (() => refreshUpdateCache({ timeoutMs: INTERACTIVE_UPDATE_CHECK_TIMEOUT_MS })), detectInstallSource: overrides.detectInstallSource ?? (() => detectInstallSource()), installUpdate: overrides.installUpdate ?? installUpdateForeground, promptForInstallChoice: overrides.promptForInstallChoice ?? promptForInstallChoice, diff --git a/apps/kimi-code/src/cli/update/cdn.ts b/apps/kimi-code/src/cli/update/cdn.ts index 6e423cdb0c0..0aca3f33997 100644 --- a/apps/kimi-code/src/cli/update/cdn.ts +++ b/apps/kimi-code/src/cli/update/cdn.ts @@ -33,11 +33,15 @@ export interface FetchLatestResult { readonly manifest: UpdateManifest | null; } -async function fetchWithTimeout(fetchImpl: typeof fetch, input: string): Promise { +async function fetchWithTimeout( + fetchImpl: typeof fetch, + input: string, + timeoutMs: number, +): Promise { const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); - }, CDN_FETCH_TIMEOUT_MS); + }, timeoutMs); try { return await fetchImpl(input, { signal: controller.signal }); } finally { @@ -57,8 +61,9 @@ async function fetchWithTimeout(fetchImpl: typeof fetch, input: string): Promise */ export async function fetchLatestVersionFromCdn( fetchImpl: typeof fetch = fetch, + timeoutMs: number = CDN_FETCH_TIMEOUT_MS, ): Promise { - const response = await fetchWithTimeout(fetchImpl, kimiCodeCdnLatestUrl()); + const response = await fetchWithTimeout(fetchImpl, kimiCodeCdnLatestUrl(), timeoutMs); if (!response.ok) { throw new Error(`CDN /latest returned HTTP ${response.status}`); } @@ -69,8 +74,11 @@ export async function fetchLatestVersionFromCdn( return raw; } -async function fetchUpdateManifestFromCdn(fetchImpl: typeof fetch): Promise { - const response = await fetchWithTimeout(fetchImpl, kimiCodeCdnLatestJsonUrl()); +async function fetchUpdateManifestFromCdn( + fetchImpl: typeof fetch, + timeoutMs: number, +): Promise { + const response = await fetchWithTimeout(fetchImpl, kimiCodeCdnLatestJsonUrl(), timeoutMs); if (!response.ok) { throw new Error(`CDN /latest.json returned HTTP ${response.status}`); } @@ -87,11 +95,12 @@ async function fetchUpdateManifestFromCdn(fetchImpl: typeof fetch): Promise { - const manifest = await fetchUpdateManifestFromCdn(fetchImpl).catch(() => null); + const manifest = await fetchUpdateManifestFromCdn(fetchImpl, timeoutMs).catch(() => null); if (manifest !== null) { return { latest: manifest.version, manifest }; } - const latest = await fetchLatestVersionFromCdn(fetchImpl); + const latest = await fetchLatestVersionFromCdn(fetchImpl, timeoutMs); return { latest, manifest: null }; } diff --git a/apps/kimi-code/src/cli/update/refresh.ts b/apps/kimi-code/src/cli/update/refresh.ts index 938a4a0fac3..a9ec67cecbf 100644 --- a/apps/kimi-code/src/cli/update/refresh.ts +++ b/apps/kimi-code/src/cli/update/refresh.ts @@ -11,13 +11,15 @@ export interface RefreshUpdateCacheDeps { readonly fetchLatest: () => Promise; readonly writeCache: (cache: UpdateCache) => Promise; readonly now: () => Date; + readonly timeoutMs?: number; } export async function refreshUpdateCache( overrides: Partial = {}, ): Promise { const resolved: RefreshUpdateCacheDeps = { - fetchLatest: overrides.fetchLatest ?? (() => fetchLatestFromCdn()), + fetchLatest: + overrides.fetchLatest ?? (() => fetchLatestFromCdn(undefined, overrides.timeoutMs)), writeCache: overrides.writeCache ?? writeUpdateCache, now: overrides.now ?? (() => new Date()), }; diff --git a/apps/kimi-code/src/constant/app.ts b/apps/kimi-code/src/constant/app.ts index 328b8190e59..e585a821ae3 100644 --- a/apps/kimi-code/src/constant/app.ts +++ b/apps/kimi-code/src/constant/app.ts @@ -122,6 +122,7 @@ export function kimiCodePluginMarketplaceUrl(): string { // marketplace versions. Without it a stalled connection to github.com hangs // the version phase for undici's default header timeout (300s). export const MARKETPLACE_VERSION_LOOKUP_TIMEOUT_MS = 5000; +export const INTERACTIVE_UPDATE_CHECK_TIMEOUT_MS = 10_000; // Official plugins whose usage bills against the user's plan quota. Installing // one of these shows a quota note after the install result. export const QUOTA_CONSUMING_PLUGIN_IDS: readonly string[] = ['kimi-datasource']; diff --git a/apps/kimi-code/test/cli/update/cdn.test.ts b/apps/kimi-code/test/cli/update/cdn.test.ts index bbfaf965b11..dae77449f09 100644 --- a/apps/kimi-code/test/cli/update/cdn.test.ts +++ b/apps/kimi-code/test/cli/update/cdn.test.ts @@ -230,4 +230,32 @@ describe('fetchLatestFromCdn', () => { vi.useRealTimers(); } }); + + it('honors a custom request timeout instead of the background budget', async () => { + vi.useFakeTimers(); + try { + const f = vi.fn(async (_input: string | URL, init?: RequestInit) => { + return new Promise((_resolve, reject) => { + init?.signal?.addEventListener('abort', () => { + reject(new Error('aborted')); + }, { once: true }); + }); + }) as unknown as typeof fetch; + + const result = fetchLatestFromCdn(f, 10_000); + let rejected = false; + void result.catch(() => { + rejected = true; + }); + const expectation = expect(result).rejects.toThrow(/aborted/); + await vi.advanceTimersByTimeAsync(6_000); + expect(rejected).toBe(false); + await vi.advanceTimersByTimeAsync(14_000); + + await expectation; + expect(rejected).toBe(true); + } finally { + vi.useRealTimers(); + } + }); }); diff --git a/apps/kimi-code/test/cli/update/refresh.test.ts b/apps/kimi-code/test/cli/update/refresh.test.ts index ceb1306f77e..ff5a340de51 100644 --- a/apps/kimi-code/test/cli/update/refresh.test.ts +++ b/apps/kimi-code/test/cli/update/refresh.test.ts @@ -62,4 +62,38 @@ describe('refreshUpdateCache', () => { expect(writeCache).not.toHaveBeenCalled(); }); + + it('threads timeoutMs into the default CDN fetch', async () => { + vi.useFakeTimers(); + vi.stubGlobal( + 'fetch', + vi.fn(async (_input: string | URL, init?: RequestInit) => { + return new Promise((_resolve, reject) => { + init?.signal?.addEventListener('abort', () => { + reject(new Error('aborted')); + }, { once: true }); + }); + }), + ); + try { + const result = refreshUpdateCache({ + timeoutMs: 10_000, + writeCache: async () => {}, + }); + let rejected = false; + void result.catch(() => { + rejected = true; + }); + const expectation = expect(result).rejects.toThrow(/aborted/); + await vi.advanceTimersByTimeAsync(6_000); + expect(rejected).toBe(false); + await vi.advanceTimersByTimeAsync(14_000); + + await expectation; + expect(rejected).toBe(true); + } finally { + vi.useRealTimers(); + vi.unstubAllGlobals(); + } + }); });