Skip to content
Open
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/fix-unc-git-probe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix chat failing to start on Windows when the workspace is on a network share (UNC path) that is not a git repository.
13 changes: 5 additions & 8 deletions packages/agent-core/src/mcp/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ async function pathExists(filePath: string): Promise<boolean> {
try {
await stat(filePath);
return true;
} catch (error: unknown) {
if (isPathMissing(error)) return false;
throw error;
} catch {
// Probing an optional marker file (e.g. `.git` while walking up parents)
// must never throw: on Windows UNC shares, stat past the share root can
// fail with UNKNOWN/EPERM instead of ENOENT.
return false;
}
}

Expand Down Expand Up @@ -156,11 +158,6 @@ function isFileNotFound(error: unknown): boolean {
return getErrorCode(error) === 'ENOENT';
}

function isPathMissing(error: unknown): boolean {
const code = getErrorCode(error);
return code === 'ENOENT' || code === 'ENOTDIR';
}

function getErrorCode(error: unknown): unknown {
if (typeof error !== 'object' || error === null || !('code' in error)) return undefined;
return (error as { code: unknown }).code;
Expand Down
35 changes: 34 additions & 1 deletion packages/agent-core/test/mcp/config-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,28 @@ import { mkdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'pathe';

import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { ErrorCodes, KimiError } from '../../src/errors';
import { loadMcpServers, resolveMcpJsonPaths } from '../../src/mcp/config-loader';

// Lets individual tests make `.git` marker probes fail with a specific error
// code (e.g. UNKNOWN on Windows UNC shares); everything else stats through.
const probeFailure = vi.hoisted(() => ({ code: undefined as string | undefined }));

vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>();
const stat = (path: Parameters<typeof actual.stat>[0], options?: Parameters<typeof actual.stat>[1]) => {
if (probeFailure.code !== undefined && String(path).endsWith('.git')) {
const error: NodeJS.ErrnoException = new Error(`unknown error, stat '${String(path)}'`);
error.code = probeFailure.code;
return Promise.reject(error);
}
return actual.stat(path, options);
};
return { ...actual, stat };
});

const tempDirs: string[] = [];

afterEach(async () => {
Expand Down Expand Up @@ -40,6 +57,22 @@ describe('resolveMcpJsonPaths', () => {
expect(paths.projectRoot).toBe(join(repoRoot, '.mcp.json'));
expect(paths.project).toBe(join(cwd, '.kimi-code', 'mcp.json'));
});

// Windows can fail a `.git` probe past a UNC share root with UNKNOWN/EPERM
// instead of ENOENT; the walk must treat that as "not found" (#2540).
it.each(['UNKNOWN', 'EPERM'])(
'falls back to the cwd when .git probes fail with %s',
async (code) => {
const cwd = makeTempDir();
probeFailure.code = code;
try {
const paths = await resolveMcpJsonPaths({ cwd, homeDir: '/home/user/.kimi-code' });
expect(paths.projectRoot).toBe(join(cwd, '.mcp.json'));
} finally {
probeFailure.code = undefined;
}
},
);
});

describe('loadMcpServers', () => {
Expand Down