mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-16 04:05:58 +00:00
fix: stop repeated file-watcher errors on Windows drive-root and UNC workspaces (#2876)
This commit is contained in:
parent
f8a88c1bd9
commit
5912d4c7d1
5 changed files with 118 additions and 24 deletions
5
.changeset/fix-windows-project-root-watch.md
Normal file
5
.changeset/fix-windows-project-root-watch.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix repeated file-watcher errors on Windows when the workspace is a drive root (such as `E:\`) or a UNC network share.
|
||||
|
|
@ -1,14 +1,40 @@
|
|||
/**
|
||||
* `_base/utils/paths` (cross-cutting) — pure path-filter predicates.
|
||||
* `_base/utils/paths` (cross-cutting) — pure path predicates and directory
|
||||
* walks.
|
||||
*
|
||||
* Constrains filesystem watches to selected subtrees and scanner-visible
|
||||
* entries.
|
||||
* entries, and walks host directory chains with platform-native path
|
||||
* semantics so drive-letter / UNC roots keep their host form.
|
||||
*/
|
||||
|
||||
import nodePath from 'node:path';
|
||||
|
||||
function normalizeSlashes(p: string): string {
|
||||
return p.replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
export interface UpwardRootPathApi {
|
||||
resolve(dir: string): string;
|
||||
dirname(dir: string): string;
|
||||
join(...segments: string[]): string;
|
||||
}
|
||||
|
||||
export async function findUpwardRoot(
|
||||
workDir: string,
|
||||
markerName: string,
|
||||
hasMarker: (markerPath: string) => Promise<boolean>,
|
||||
pathApi: UpwardRootPathApi = nodePath,
|
||||
): Promise<string> {
|
||||
const start = pathApi.resolve(workDir);
|
||||
let current = start;
|
||||
while (true) {
|
||||
if (await hasMarker(pathApi.join(current, markerName))) return normalizeSlashes(current);
|
||||
const parent = pathApi.dirname(current);
|
||||
if (parent === current) return normalizeSlashes(start);
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SubtreeWatchFilterOptions {
|
||||
readonly maxDepth?: number;
|
||||
readonly skipEntry?: (entryName: string) => boolean;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
import { promises as fs } from 'node:fs';
|
||||
import path from 'pathe';
|
||||
|
||||
import { findUpwardRoot } from '#/_base/utils/paths';
|
||||
|
||||
import type { SkillRoot, SkillSource } from './types';
|
||||
|
||||
const USER_BRAND_DIRS = ['skills'] as const;
|
||||
|
|
@ -78,14 +80,7 @@ export async function configuredRoots(
|
|||
}
|
||||
|
||||
async function findProjectRoot(workDir: string): Promise<string> {
|
||||
const start = path.resolve(workDir);
|
||||
let current = start;
|
||||
while (true) {
|
||||
if (await exists(path.join(current, '.git'))) return current;
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) return start;
|
||||
current = parent;
|
||||
}
|
||||
return findUpwardRoot(workDir, '.git', exists);
|
||||
}
|
||||
|
||||
async function pushFirstExisting(
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
* filesystem boundary. Pure path probes; no scoped state.
|
||||
*/
|
||||
|
||||
import { dirname, join, resolve } from 'pathe';
|
||||
import { join } from 'pathe';
|
||||
|
||||
import { findUpwardRoot } from '#/_base/utils/paths';
|
||||
import type { IHostFileSystem } from '#/os/interface/hostFileSystem';
|
||||
import { HostFsError, OsFsErrors } from '#/os/interface/hostFsErrors';
|
||||
|
||||
|
|
@ -86,20 +87,15 @@ async function findProjectRoot(
|
|||
workDir: string,
|
||||
warn?: AgentRootWarn,
|
||||
): Promise<string> {
|
||||
const start = resolve(workDir);
|
||||
let current = start;
|
||||
while (true) {
|
||||
const marker = join(current, '.git');
|
||||
return findUpwardRoot(workDir, '.git', async (marker) => {
|
||||
try {
|
||||
if (await pathExists(fs, marker)) return current;
|
||||
return await pathExists(fs, marker);
|
||||
} catch (error) {
|
||||
if (isUnavailable(error)) throw error;
|
||||
warn?.(`Skipping unreadable project marker ${marker}: ${errorMessage(error)}`, error);
|
||||
return false;
|
||||
}
|
||||
const parent = dirname(current);
|
||||
if (parent === current) return start;
|
||||
current = parent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function pushFirstExisting(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
/**
|
||||
* Scenario: recursive watches constrained to selected candidate subtrees.
|
||||
* Responsibilities: candidate ancestry, scan-depth bounds, and excluded-entry
|
||||
* probing. Wiring: pure path predicates with no external collaborators.
|
||||
* Responsibilities: candidate ancestry, scan-depth bounds, excluded-entry
|
||||
* probing, and the marker-based upward root walk. Wiring: pure path
|
||||
* predicates and walks with no external collaborators.
|
||||
* Run: `pnpm --filter @moonshot-ai/agent-core-v2 exec vitest run
|
||||
* test/_base/utils/paths.test.ts`.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mkdtemp, mkdir, rm, stat } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import nodePath, { win32 } from 'node:path';
|
||||
|
||||
import { subtreeWatchFilter } from '#/_base/utils/paths';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { findUpwardRoot, subtreeWatchFilter } from '#/_base/utils/paths';
|
||||
|
||||
describe('subtree watch filtering', () => {
|
||||
const root = '/repo';
|
||||
|
|
@ -105,3 +110,70 @@ describe('subtree watch filtering', () => {
|
|||
expect(ignored('/repo/.agents/skills/parent/child/runtime')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUpwardRoot', () => {
|
||||
const noMarker = async () => false;
|
||||
|
||||
describe('with host-default path semantics', () => {
|
||||
let root: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(nodePath.join(tmpdir(), 'upward-root-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const hasMarker = async (markerPath: string): Promise<boolean> => {
|
||||
try {
|
||||
await stat(markerPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
it('stops at the nearest ancestor holding the marker', async () => {
|
||||
await mkdir(nodePath.join(root, '.git'));
|
||||
const child = nodePath.join(root, 'src', 'pkg');
|
||||
await mkdir(child, { recursive: true });
|
||||
|
||||
const found = await findUpwardRoot(child, '.git', hasMarker);
|
||||
|
||||
expect(found).toBe(root.replaceAll('\\', '/'));
|
||||
});
|
||||
|
||||
it('falls back to the working directory when no ancestor holds the marker', async () => {
|
||||
const child = nodePath.join(root, 'src', 'pkg');
|
||||
await mkdir(child, { recursive: true });
|
||||
|
||||
const found = await findUpwardRoot(child, '.git', hasMarker);
|
||||
|
||||
expect(found).toBe(child.replaceAll('\\', '/'));
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps a Windows drive-root working directory in host form', async () => {
|
||||
const found = await findUpwardRoot('E:\\', '.git', noMarker, win32);
|
||||
|
||||
expect(found).toBe('E:/');
|
||||
});
|
||||
|
||||
it('keeps a Windows UNC working directory in host form', async () => {
|
||||
const found = await findUpwardRoot('\\\\fs1\\share\\dir', '.git', noMarker, win32);
|
||||
|
||||
expect(found).toBe('//fs1/share/dir');
|
||||
});
|
||||
|
||||
it('stops at the nearest Windows ancestor holding the marker', async () => {
|
||||
const found = await findUpwardRoot(
|
||||
'E:\\repo\\src',
|
||||
'.git',
|
||||
async (markerPath) => markerPath === 'E:\\repo\\.git',
|
||||
win32,
|
||||
);
|
||||
|
||||
expect(found).toBe('E:/repo');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue