fix(cli): validate config.toml against the v2 schema in kimi doctor (#3372)
Some checks are pending
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-vscode-legacy (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Publish VS Code extension / Publish VSIX to marketplaces (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / build (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions

This commit is contained in:
Haozhe 2026-08-30 15:30:09 +08:00 committed by GitHub
parent 961927739e
commit 56b5480ed0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 50 additions and 39 deletions

View file

@ -1,7 +1,7 @@
/**
* Agent engine routing gates for the CLI surfaces.
*
* `kimi -p`, the interactive TUI, and `kimi doctor` use the native
* `kimi -p` and the interactive TUI use the native
* agent-core-v2 path by default. A truthy `KIMI_CODE_LEGACY_FLAG` selects the
* legacy agent-core-backed path instead. `KIMI_CODE_EXPERIMENTAL_FLAG` remains
* the master switch for experimental features within either engine; it does

View file

@ -2,15 +2,10 @@ import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { isAbsolute, resolve } from 'node:path';
import {
createKimiConfigRpc,
type KimiConfigRpc,
type KimiConfigValidationIssue,
} from '@moonshot-ai/kimi-code-sdk';
import { resolveConfigPath, type KimiConfigValidationIssue } from '@moonshot-ai/kimi-code-sdk';
import type { Command } from 'commander';
import { z } from 'zod';
import { isKimiV2Enabled } from '#/cli/experimental-v2';
import { getTuiConfigPath, parseTuiConfig } from '#/tui/config';
interface WritableLike {
@ -26,7 +21,6 @@ export interface DoctorDeps {
readonly stdout: WritableLike;
readonly stderr: WritableLike;
readonly exit: (code: number) => never;
readonly configRpc?: KimiConfigRpc;
readonly fileExists?: (path: string) => boolean;
readonly readTextFile?: (path: string) => Promise<string>;
readonly validateConfigToml?: (text: string, path: string) => MaybePromise<string | void>;
@ -115,15 +109,9 @@ async function runDoctorCommand(
}
function resolveDeps(deps: Partial<DoctorDeps> | DoctorDeps | undefined): ResolvedDoctorDeps {
let configRpc = deps?.configRpc;
const getConfigRpc = (): KimiConfigRpc => {
configRpc ??= createKimiConfigRpc();
return configRpc;
};
return {
cwd: deps?.cwd ?? (() => process.cwd()),
defaultConfigPath: deps?.defaultConfigPath ?? (() => getConfigRpc().resolveConfigPath()),
defaultConfigPath: deps?.defaultConfigPath ?? (() => resolveConfigPath({})),
defaultTuiConfigPath: deps?.defaultTuiConfigPath ?? getTuiConfigPath,
stdout: deps?.stdout ?? process.stdout,
stderr: deps?.stderr ?? process.stderr,
@ -133,15 +121,8 @@ function resolveDeps(deps: Partial<DoctorDeps> | DoctorDeps | undefined): Resolv
validateConfigToml:
deps?.validateConfigToml ??
(async (text, filePath) => {
if (isKimiV2Enabled()) {
// Default v2 route (same engine gate as `kimi -p`): validate with
// the agent-core-v2 section registry instead of the legacy schema.
// Loaded lazily so the v2 module graph stays off the legacy path.
const { validateConfigTomlV2 } = await import('../v2/validate-config');
return validateConfigTomlV2(text, filePath);
}
await getConfigRpc().validateConfigToml({ text, filePath });
return undefined;
const { validateConfigTomlV2 } = await import('../v2/validate-config');
return validateConfigTomlV2(text, filePath);
}),
};
}

View file

@ -14,7 +14,6 @@ import {
let dir: string;
beforeEach(async () => {
vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '');
dir = join(tmpdir(), `kimi-doctor-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(dir, { recursive: true });
});
@ -103,25 +102,56 @@ describe('kimi doctor', () => {
expect(out).toContain('built-in defaults will apply');
});
it('uses the legacy validator when legacy wins over the experimental flag', async () => {
it('keeps v2 validation when the legacy flag is set', async () => {
const configPath = join(dir, 'config.toml');
const text = '[providers.kimi]\ntype = "kimi"\n';
await writeFile(configPath, text, 'utf-8');
vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1');
vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '1');
const validateConfigToml = vi.fn(async () => undefined);
const { deps } = makeDeps();
await writeFile(
configPath,
`
default_model = "kimi"
const code = await handleDoctor(
{
...deps,
configRpc: { validateConfigToml } as unknown as NonNullable<DoctorDeps['configRpc']>,
},
{ target: 'config' },
[providers.kimi]
type = "kimi"
base_url = "https://api.example.com/v1"
api_key = "YOUR_API_KEY"
[models.kimi]
provider = "kimi"
model = "kimi"
protocol = "openai"
max_context_size = 262144
`,
'utf-8',
);
vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1');
const { deps, stdout, stderr } = makeDeps();
const code = await handleDoctor(deps, { target: 'config' });
expect(code).toBe(0);
expect(validateConfigToml).toHaveBeenCalledWith({ text, filePath: configPath });
expect(stderr.join('')).toBe('');
expect(stdout.join('')).toContain(`OK config.toml ${configPath}`);
});
it('reports schema-invalid sections with the v2 engine when the legacy flag is set', async () => {
await writeFile(
join(dir, 'config.toml'),
`
[models.kimi]
provider = "kimi"
model = "kimi"
max_context_size = "large"
`,
'utf-8',
);
vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1');
const { deps, stderr } = makeDeps();
const code = await handleDoctor(deps, { target: 'config' });
expect(code).toBe(1);
const err = stderr.join('');
expect(err).toContain('Validation issues:');
expect(err).toContain('models.kimi.max_context_size:');
});
it('checks only config.toml when the config target is selected', async () => {