Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/interactive-update-check-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Increase the request timeout for `kimi update`.
6 changes: 5 additions & 1 deletion apps/kimi-code/src/cli/sub/upgrade.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -174,7 +176,9 @@ export async function handleUpgrade(

function createDefaultUpgradeDeps(overrides: Partial<UpgradeDeps>): 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,
Expand Down
23 changes: 16 additions & 7 deletions apps/kimi-code/src/cli/update/cdn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ export interface FetchLatestResult {
readonly manifest: UpdateManifest | null;
}

async function fetchWithTimeout(fetchImpl: typeof fetch, input: string): Promise<Response> {
async function fetchWithTimeout(
fetchImpl: typeof fetch,
input: string,
timeoutMs: number,
): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, CDN_FETCH_TIMEOUT_MS);
}, timeoutMs);
try {
return await fetchImpl(input, { signal: controller.signal });
} finally {
Expand All @@ -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<string> {
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}`);
}
Expand All @@ -69,8 +74,11 @@ export async function fetchLatestVersionFromCdn(
return raw;
}

async function fetchUpdateManifestFromCdn(fetchImpl: typeof fetch): Promise<UpdateManifest> {
const response = await fetchWithTimeout(fetchImpl, kimiCodeCdnLatestJsonUrl());
async function fetchUpdateManifestFromCdn(
fetchImpl: typeof fetch,
timeoutMs: number,
): Promise<UpdateManifest> {
const response = await fetchWithTimeout(fetchImpl, kimiCodeCdnLatestJsonUrl(), timeoutMs);
if (!response.ok) {
throw new Error(`CDN /latest.json returned HTTP ${response.status}`);
}
Expand All @@ -87,11 +95,12 @@ async function fetchUpdateManifestFromCdn(fetchImpl: typeof fetch): Promise<Upda
*/
export async function fetchLatestFromCdn(
fetchImpl: typeof fetch = fetch,
timeoutMs: number = CDN_FETCH_TIMEOUT_MS,
): Promise<FetchLatestResult> {
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 };
}
4 changes: 3 additions & 1 deletion apps/kimi-code/src/cli/update/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ export interface RefreshUpdateCacheDeps {
readonly fetchLatest: () => Promise<FetchLatestResult>;
readonly writeCache: (cache: UpdateCache) => Promise<void>;
readonly now: () => Date;
readonly timeoutMs?: number;
}

export async function refreshUpdateCache(
overrides: Partial<RefreshUpdateCacheDeps> = {},
): Promise<UpdateCache> {
const resolved: RefreshUpdateCacheDeps = {
fetchLatest: overrides.fetchLatest ?? (() => fetchLatestFromCdn()),
fetchLatest:
overrides.fetchLatest ?? (() => fetchLatestFromCdn(undefined, overrides.timeoutMs)),
writeCache: overrides.writeCache ?? writeUpdateCache,
now: overrides.now ?? (() => new Date()),
};
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/constant/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
28 changes: 28 additions & 0 deletions apps/kimi-code/test/cli/update/cdn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response>((_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();
}
});
});
34 changes: 34 additions & 0 deletions apps/kimi-code/test/cli/update/refresh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response>((_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();
}
});
});
Loading