From b7ffda05a8f2ba96314409d1f7a5539a575035b1 Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Thu, 4 Jun 2026 20:25:04 +0800 Subject: [PATCH] fix: avoid runtime check rpc --- apps/kimi-code/src/cli/run-prompt.ts | 1 - apps/kimi-code/src/cli/run-shell.ts | 31 ++++--- apps/kimi-code/test/cli/goal-prompt.test.ts | 2 - apps/kimi-code/test/cli/run-prompt.test.ts | 18 ++-- apps/kimi-code/test/cli/run-shell.test.ts | 39 --------- packages/agent-core/src/rpc/core-api.ts | 1 - packages/agent-core/src/rpc/core-impl.ts | 4 - .../agent-core/test/harness/runtime.test.ts | 82 +++++++++++++++++++ packages/node-sdk/src/kimi-harness.ts | 4 - packages/node-sdk/src/rpc.ts | 5 -- 10 files changed, 102 insertions(+), 85 deletions(-) diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index 3fcd5fb1a5..7d41e6246e 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -110,7 +110,6 @@ export async function runPrompt( removeTerminationCleanup = installPromptTerminationCleanup(promptProcess, cleanupPromptRun); try { - await harness.checkRuntimeEnvironment(); await harness.ensureConfigFile(); const config = await harness.getConfig(); const { session, resumed, restorePermission, telemetryModel, goalModel } = diff --git a/apps/kimi-code/src/cli/run-shell.ts b/apps/kimi-code/src/cli/run-shell.ts index 3633f41cbd..21e23602ba 100644 --- a/apps/kimi-code/src/cli/run-shell.ts +++ b/apps/kimi-code/src/cli/run-shell.ts @@ -34,6 +34,21 @@ export async function runShell( runOptions: { readonly migrateOnly?: boolean } = {}, ): Promise { const startedAt = Date.now(); + const configStartedAt = startedAt; + let tuiConfig: TuiConfig; + let configWarning: string | undefined; + try { + tuiConfig = await loadTuiConfig(); + } catch (error) { + if (!(error instanceof TuiConfigParseError)) throw error; + tuiConfig = error.fallback; + configWarning = error.message; + } + + // Resolve `theme = "auto"` against the live terminal once, before pi-tui + // grabs stdin. Explicit `dark` / `light` skip detection. + const resolvedTheme = tuiConfig.theme === 'auto' ? await detectTerminalTheme() : tuiConfig.theme; + const workDir = process.cwd(); const telemetryBootstrap = createCliTelemetryBootstrap(); const telemetryClient: TelemetryClient = { @@ -63,22 +78,6 @@ export async function runShell( platform: `${process.platform}/${process.arch}`, workDir, }); - await harness.checkRuntimeEnvironment(); - - const configStartedAt = Date.now(); - let tuiConfig: TuiConfig; - let configWarning: string | undefined; - try { - tuiConfig = await loadTuiConfig(); - } catch (error) { - if (!(error instanceof TuiConfigParseError)) throw error; - tuiConfig = error.fallback; - configWarning = error.message; - } - - // Resolve `theme = "auto"` against the live terminal once, before pi-tui - // grabs stdin. Explicit `dark` / `light` skip detection. - const resolvedTheme = tuiConfig.theme === 'auto' ? await detectTerminalTheme() : tuiConfig.theme; await harness.ensureConfigFile(); const migrationPlan = await detectPendingMigration({ diff --git a/apps/kimi-code/test/cli/goal-prompt.test.ts b/apps/kimi-code/test/cli/goal-prompt.test.ts index 5373c04a10..b7966a5a38 100644 --- a/apps/kimi-code/test/cli/goal-prompt.test.ts +++ b/apps/kimi-code/test/cli/goal-prompt.test.ts @@ -112,7 +112,6 @@ const mocks = vi.hoisted(() => { mainEvent, experimentalFeatures: [{ id: 'goal_command', enabled: true }], sessions: [] as Array<{ readonly id: string; readonly workDir: string }>, - harnessCheckRuntimeEnvironment: vi.fn(async () => undefined), }; }); @@ -123,7 +122,6 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { createKimiHarness: () => ({ homeDir: '/tmp/kimi-goal-home', auth: { getCachedAccessToken: vi.fn() }, - checkRuntimeEnvironment: mocks.harnessCheckRuntimeEnvironment, ensureConfigFile: vi.fn(), getConfig: vi.fn(async () => ({ providers: {}, defaultModel: 'k2', telemetry: true })), getExperimentalFeatures: vi.fn(async () => mocks.experimentalFeatures), diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts index 713231ee3c..28ba20df1e 100644 --- a/apps/kimi-code/test/cli/run-prompt.test.ts +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -46,7 +46,6 @@ const mocks = vi.hoisted(() => { agentEvent, mainEvent, kimiHarnessConstructor: vi.fn(), - harnessCheckRuntimeEnvironment: vi.fn(async () => undefined), harnessEnsureConfigFile: vi.fn(), harnessGetConfig: vi.fn( async (): Promise<{ providers: {}; defaultModel?: string; telemetry: boolean }> => ({ @@ -90,7 +89,6 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { return { homeDir, auth: { getCachedAccessToken: mocks.harnessGetCachedAccessToken }, - checkRuntimeEnvironment: mocks.harnessCheckRuntimeEnvironment, ensureConfigFile: mocks.harnessEnsureConfigFile, getConfig: mocks.harnessGetConfig, getExperimentalFeatures: mocks.harnessGetExperimentalFeatures, @@ -189,7 +187,6 @@ describe('runPrompt', () => { mocks.resolveKimiHome.mockImplementation( (homeDir?: string) => homeDir ?? '/tmp/kimi-code-test-home', ); - mocks.harnessCheckRuntimeEnvironment.mockResolvedValue(undefined); mocks.harnessCreatesDeviceIdOnConstruction = false; }); @@ -202,10 +199,6 @@ describe('runPrompt', () => { expect(mocks.kimiHarnessConstructor).toHaveBeenCalledWith( expect.objectContaining({ skillDirs: ['/skills'], uiMode: 'print' }), ); - expect(mocks.harnessCheckRuntimeEnvironment).toHaveBeenCalledOnce(); - expect(mocks.harnessCheckRuntimeEnvironment.mock.invocationCallOrder[0]).toBeLessThan( - mocks.harnessEnsureConfigFile.mock.invocationCallOrder[0]!, - ); expect(mocks.harnessCreateSession).toHaveBeenCalledWith({ workDir: process.cwd(), model: 'k2', @@ -221,19 +214,18 @@ describe('runPrompt', () => { expect(mocks.harnessClose).toHaveBeenCalled(); }); - it('stops prompt startup when runtime environment check fails', async () => { + it('stops prompt startup when session creation fails', async () => { const stdout = writer(); const stderr = writer(); - mocks.harnessCheckRuntimeEnvironment.mockRejectedValueOnce(new Error('Git Bash missing')); + mocks.harnessCreateSession.mockRejectedValueOnce(new Error('Git Bash missing')); await expect(runPrompt(opts(), '1.2.3-test', { stdout, stderr })).rejects.toThrow( 'Git Bash missing', ); - expect(mocks.harnessCheckRuntimeEnvironment).toHaveBeenCalledOnce(); - expect(mocks.harnessEnsureConfigFile).not.toHaveBeenCalled(); - expect(mocks.harnessGetConfig).not.toHaveBeenCalled(); - expect(mocks.harnessCreateSession).not.toHaveBeenCalled(); + expect(mocks.harnessEnsureConfigFile).toHaveBeenCalledOnce(); + expect(mocks.harnessGetConfig).toHaveBeenCalledOnce(); + expect(mocks.harnessCreateSession).toHaveBeenCalledOnce(); expect(mocks.session.prompt).not.toHaveBeenCalled(); expect(mocks.harnessClose).toHaveBeenCalledOnce(); }); diff --git a/apps/kimi-code/test/cli/run-shell.test.ts b/apps/kimi-code/test/cli/run-shell.test.ts index 027aeb85ff..c55a5e5904 100644 --- a/apps/kimi-code/test/cli/run-shell.test.ts +++ b/apps/kimi-code/test/cli/run-shell.test.ts @@ -31,7 +31,6 @@ const mocks = vi.hoisted(() => { loadTuiConfig: vi.fn(), detectTerminalTheme: vi.fn(), kimiHarnessConstructor: vi.fn(), - harnessCheckRuntimeEnvironment: vi.fn(async () => undefined), harnessEnsureConfigFile: vi.fn(), harnessGetConfig: vi.fn(async () => ({ providers: {}, @@ -82,7 +81,6 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { getCachedAccessToken: mocks.harnessGetCachedAccessToken, }, ensureConfigFile: mocks.harnessEnsureConfigFile, - checkRuntimeEnvironment: mocks.harnessCheckRuntimeEnvironment, getConfig: mocks.harnessGetConfig, close: mocks.harnessClose, track: mocks.harnessTrack, @@ -151,7 +149,6 @@ describe('runShell', () => { defaultModel: 'k2', telemetry: true, }); - mocks.harnessCheckRuntimeEnvironment.mockResolvedValue(undefined); mocks.tuiGetStartupMcpMs.mockResolvedValue(0); mocks.tuiGetCurrentSessionId.mockReturnValue(''); mocks.tuiHasSessionContent.mockReturnValue(false); @@ -194,10 +191,6 @@ describe('runShell', () => { }), }), ); - expect(mocks.harnessCheckRuntimeEnvironment).toHaveBeenCalledOnce(); - expect(mocks.harnessCheckRuntimeEnvironment.mock.invocationCallOrder[0]).toBeLessThan( - mocks.harnessEnsureConfigFile.mock.invocationCallOrder[0]!, - ); expect(mocks.harnessEnsureConfigFile).toHaveBeenCalledOnce(); expect(mocks.harnessEnsureConfigFile.mock.invocationCallOrder[0]).toBeLessThan( mocks.harnessGetConfig.mock.invocationCallOrder[0]!, @@ -251,38 +244,6 @@ describe('runShell', () => { }); }); - it('stops startup when runtime environment check fails', async () => { - mocks.loadTuiConfig.mockResolvedValue({ - theme: 'dark', - editorCommand: null, - notifications: { enabled: true, condition: 'unfocused' }, - }); - mocks.harnessCheckRuntimeEnvironment.mockRejectedValueOnce(new Error('Git Bash missing')); - - await expect( - runShell( - { - session: undefined, - continue: false, - yolo: false, - auto: false, - plan: false, - model: undefined, - outputFormat: undefined, - prompt: undefined, - skillsDirs: [], - }, - '1.2.3-test', - ), - ).rejects.toThrow('Git Bash missing'); - - expect(mocks.harnessCheckRuntimeEnvironment).toHaveBeenCalledOnce(); - expect(mocks.harnessEnsureConfigFile).not.toHaveBeenCalled(); - expect(mocks.harnessGetConfig).not.toHaveBeenCalled(); - expect(mocks.kimiTuiConstructor).not.toHaveBeenCalled(); - expect(mocks.tuiStart).not.toHaveBeenCalled(); - }); - it('tracks first launch when device id creation reports first launch', async () => { mocks.loadTuiConfig.mockResolvedValue({ theme: 'dark', diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts index b5942c73e8..da186c7218 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -357,7 +357,6 @@ type SessionAPIWithId = WithSessionId; export interface CoreAPI extends SessionAPIWithId { getCoreInfo: (payload: EmptyPayload) => CoreInfo; - checkRuntimeEnvironment: (payload: EmptyPayload) => void; getExperimentalFeatures: (payload: EmptyPayload) => readonly ExperimentalFeatureState[]; getKimiConfig: (payload: GetKimiConfigPayload) => KimiConfig; setKimiConfig: (payload: SetKimiConfigPayload) => KimiConfig; diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index 48fb9bacf5..7b061c1163 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -176,10 +176,6 @@ export class KimiCore implements PromisableMethods { this.sdk = rpcClient(this); } - async checkRuntimeEnvironment(_: EmptyPayload): Promise { - await this.getKaos(); - } - async createSession(input: CreateSessionPayload): Promise { const options = input; const workDir = requiredWorkDir('createSession', options.workDir); diff --git a/packages/agent-core/test/harness/runtime.test.ts b/packages/agent-core/test/harness/runtime.test.ts index 4030c85180..732a2f8253 100644 --- a/packages/agent-core/test/harness/runtime.test.ts +++ b/packages/agent-core/test/harness/runtime.test.ts @@ -2,6 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'pathe'; +import type { Kaos } from '@moonshot-ai/kaos'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { @@ -10,6 +11,7 @@ import { createRPC, ErrorCodes, KimiCore, + KimiError, type ApprovalResponse, type CoreAPI, type SDKAPI, @@ -21,6 +23,7 @@ import { } from '../../src/logging/logger'; import { resolveLoggingConfig } from '../../src/logging/resolve-config'; import type { OAuthTokenProviderResolver } from '../../src/session/provider-manager'; +import { testKaos } from '../fixtures/test-kaos'; function requiredFlagEnv(id: string): string { const def = FLAG_DEFINITIONS.find((item) => item.id === id); @@ -39,6 +42,16 @@ function experimentalFeatureEnabled(core: KimiCore, id: string): boolean | undef return core.getExperimentalFeatures().find((feature) => feature.id === id)?.enabled; } +function setCoreKaos(core: KimiCore, kaos: Promise): void { + (core as unknown as { kaos?: Promise }).kaos = kaos; +} + +function rejectedKaos(error: Error): Promise { + const promise = Promise.reject(error) as Promise; + promise.catch(() => undefined); + return promise; +} + describe('KimiCore runtime config', () => { let tmp: string; @@ -284,6 +297,75 @@ max_context_size = 100000 expect(mainAgent?.config.modelAlias).toBe('default-mock'); }); + it('rejects createSession when shell runtime initialization fails', async () => { + tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-')); + const homeDir = join(tmp, 'home'); + const workDir = join(tmp, 'work'); + await mkdir(homeDir, { recursive: true }); + await mkdir(workDir, { recursive: true }); + await writeFile(join(homeDir, 'config.toml'), baseModelConfig()); + + const [coreRpc, sdkRpc] = createRPC(); + const core = new KimiCore(coreRpc, { homeDir }); + const rpc = await sdkRpc({ + emitEvent: vi.fn(), + requestApproval: vi.fn(async (): Promise => ({ decision: 'rejected' })), + requestQuestion: vi.fn(async () => null), + toolCall: vi.fn(async () => ({ output: '' })), + }); + setCoreKaos( + core, + rejectedKaos( + new KimiError(ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, 'Git Bash missing'), + ), + ); + + await expect( + rpc.createSession({ + id: 'ses_runtime_shell_missing_create', + workDir, + model: 'default-mock', + }), + ).rejects.toMatchObject({ code: ErrorCodes.SHELL_GIT_BASH_NOT_FOUND }); + expect(core.sessions.has('ses_runtime_shell_missing_create')).toBe(false); + }); + + it('rejects resumeSession when shell runtime initialization fails', async () => { + tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-')); + const homeDir = join(tmp, 'home'); + const workDir = join(tmp, 'work'); + await mkdir(homeDir, { recursive: true }); + await mkdir(workDir, { recursive: true }); + await writeFile(join(homeDir, 'config.toml'), baseModelConfig()); + + const [coreRpc, sdkRpc] = createRPC(); + const core = new KimiCore(coreRpc, { homeDir }); + const rpc = await sdkRpc({ + emitEvent: vi.fn(), + requestApproval: vi.fn(async (): Promise => ({ decision: 'rejected' })), + requestQuestion: vi.fn(async () => null), + toolCall: vi.fn(async () => ({ output: '' })), + }); + setCoreKaos(core, Promise.resolve(testKaos)); + const created = await rpc.createSession({ + id: 'ses_runtime_shell_missing_resume', + workDir, + model: 'default-mock', + }); + await rpc.closeSession({ sessionId: created.id }); + setCoreKaos( + core, + rejectedKaos( + new KimiError(ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, 'Git Bash missing'), + ), + ); + + await expect(rpc.resumeSession({ sessionId: created.id })).rejects.toMatchObject({ + code: ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, + }); + expect(core.sessions.has(created.id)).toBe(false); + }); + it('reloads an active session with fresh runtime services from config.toml', async () => { tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-')); const homeDir = join(tmp, 'home'); diff --git a/packages/node-sdk/src/kimi-harness.ts b/packages/node-sdk/src/kimi-harness.ts index 47780b8559..86175843c1 100644 --- a/packages/node-sdk/src/kimi-harness.ts +++ b/packages/node-sdk/src/kimi-harness.ts @@ -210,10 +210,6 @@ export class KimiHarness { await this.ensureConfigFileImpl(); } - async checkRuntimeEnvironment(): Promise { - await this.rpc.checkRuntimeEnvironment(); - } - async setConfig(patch: KimiConfigPatch): Promise { return this.rpc.setConfig(patch); } diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index ddd384f9c5..c3a220ea87 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -99,11 +99,6 @@ export abstract class SDKRpcClientBase { protected abstract getRpc(): Promise; - async checkRuntimeEnvironment(): Promise { - const rpc = await this.getRpc(); - return rpc.checkRuntimeEnvironment({}); - } - async createSession(input: CreateSessionOptions): Promise { const rpc = await this.getRpc(); const { planMode, ...coreInput } = input;