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
1 change: 0 additions & 1 deletion apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand Down
31 changes: 15 additions & 16 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ export async function runShell(
runOptions: { readonly migrateOnly?: boolean } = {},
): Promise<void> {
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 = {
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 0 additions & 2 deletions apps/kimi-code/test/cli/goal-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
});

Expand All @@ -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),
Expand Down
18 changes: 5 additions & 13 deletions apps/kimi-code/test/cli/run-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> => ({
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -189,7 +187,6 @@ describe('runPrompt', () => {
mocks.resolveKimiHome.mockImplementation(
(homeDir?: string) => homeDir ?? '/tmp/kimi-code-test-home',
);
mocks.harnessCheckRuntimeEnvironment.mockResolvedValue(undefined);
mocks.harnessCreatesDeviceIdOnConstruction = false;
});

Expand All @@ -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',
Expand All @@ -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();
});
Expand Down
39 changes: 0 additions & 39 deletions apps/kimi-code/test/cli/run-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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]!,
Expand Down Expand Up @@ -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',
Expand Down
1 change: 0 additions & 1 deletion packages/agent-core/src/rpc/core-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,6 @@ type SessionAPIWithId = WithSessionId<SessionAPI>;

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;
Expand Down
4 changes: 0 additions & 4 deletions packages/agent-core/src/rpc/core-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,6 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
this.sdk = rpcClient(this);
}

async checkRuntimeEnvironment(_: EmptyPayload): Promise<void> {
await this.getKaos();
}

async createSession(input: CreateSessionPayload): Promise<SessionSummary> {
const options = input;
const workDir = requiredWorkDir('createSession', options.workDir);
Expand Down
82 changes: 82 additions & 0 deletions packages/agent-core/test/harness/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -10,6 +11,7 @@ import {
createRPC,
ErrorCodes,
KimiCore,
KimiError,
type ApprovalResponse,
type CoreAPI,
type SDKAPI,
Expand All @@ -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);
Expand All @@ -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<Kaos>): void {
(core as unknown as { kaos?: Promise<Kaos> }).kaos = kaos;
}

function rejectedKaos(error: Error): Promise<Kaos> {
const promise = Promise.reject(error) as Promise<Kaos>;
promise.catch(() => undefined);
return promise;
}

describe('KimiCore runtime config', () => {
let tmp: string;

Expand Down Expand Up @@ -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<CoreAPI, SDKAPI>();
const core = new KimiCore(coreRpc, { homeDir });
const rpc = await sdkRpc({
emitEvent: vi.fn(),
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ 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<CoreAPI, SDKAPI>();
const core = new KimiCore(coreRpc, { homeDir });
const rpc = await sdkRpc({
emitEvent: vi.fn(),
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ 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');
Expand Down
4 changes: 0 additions & 4 deletions packages/node-sdk/src/kimi-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,6 @@ export class KimiHarness {
await this.ensureConfigFileImpl();
}

async checkRuntimeEnvironment(): Promise<void> {
await this.rpc.checkRuntimeEnvironment();
}

async setConfig(patch: KimiConfigPatch): Promise<KimiConfig> {
return this.rpc.setConfig(patch);
}
Expand Down
5 changes: 0 additions & 5 deletions packages/node-sdk/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,6 @@ export abstract class SDKRpcClientBase {

protected abstract getRpc(): Promise<ResolvedCoreAPI>;

async checkRuntimeEnvironment(): Promise<void> {
const rpc = await this.getRpc();
return rpc.checkRuntimeEnvironment({});
}

async createSession(input: CreateSessionOptions): Promise<SessionSummary> {
const rpc = await this.getRpc();
const { planMode, ...coreInput } = input;
Expand Down
Loading