mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-22 23:26:12 +00:00
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
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
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
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
Add a shell path bridge that translates between win32 paths and the MSYS2/Git Bash path dialect. File tools resolve model-supplied paths through it before canonicalization and workspace checks: drive-letter forms translate lexically, root-relative paths resolve via cygpath -w with per-segment caching, and every failure mode falls back to the previous behavior. Fixes #2199
267 lines
9.2 KiB
TypeScript
267 lines
9.2 KiB
TypeScript
/**
|
|
* Shell path bridge — drives `createShellPathBridge` with injected
|
|
* `execFileSync` / `isFile` fakes (no real processes): lexical drive forms,
|
|
* pass-through tiers, cygpath resolution and caching, `toShellPath`.
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
createShellPathBridge,
|
|
type ShellPathBridgeDeps,
|
|
type ShellPathBridgeEnv,
|
|
} from '#/shell-path-bridge';
|
|
|
|
const WINDOWS_ENV: ShellPathBridgeEnv = {
|
|
osKind: 'Windows',
|
|
shellName: 'bash',
|
|
shellPath: 'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
};
|
|
|
|
const POSIX_ENV: ShellPathBridgeEnv = {
|
|
osKind: 'Linux',
|
|
shellName: 'bash',
|
|
shellPath: '/bin/bash',
|
|
};
|
|
|
|
// cygpath.exe candidates probed for `C:\Program Files\Git\bin\bash.exe`.
|
|
const BIN_CYGPATH = 'C:\\Program Files\\Git\\bin\\cygpath.exe';
|
|
const USR_BIN_CYGPATH = 'C:\\Program Files\\Git\\usr\\bin\\cygpath.exe';
|
|
|
|
interface StubOpts {
|
|
readonly existingPaths?: readonly string[];
|
|
readonly execFileResults?: Readonly<Record<string, string>>;
|
|
readonly execFileSync?: ShellPathBridgeDeps['execFileSync'];
|
|
}
|
|
|
|
function stubDeps(opts: StubOpts = {}) {
|
|
const existing = new Set(opts.existingPaths ?? []);
|
|
const execFileSync = vi.fn(
|
|
opts.execFileSync ??
|
|
((file: string, args: readonly string[]): string => {
|
|
const result = opts.execFileResults?.[[file, ...args].join(' ')];
|
|
if (result === undefined) throw new Error(`unexpected execFileSync: ${file}`);
|
|
return result;
|
|
}),
|
|
);
|
|
const deps: ShellPathBridgeDeps = {
|
|
execFileSync,
|
|
isFile: (path: string) => existing.has(path),
|
|
};
|
|
return { deps, execFileSync };
|
|
}
|
|
|
|
function cygpathKey(firstSegment: string): string {
|
|
return `${USR_BIN_CYGPATH} -w -C UTF8 -- /${firstSegment}`;
|
|
}
|
|
|
|
describe('fromShellPath lexical drive forms', () => {
|
|
const cases: ReadonlyArray<readonly [string, string]> = [
|
|
['/c:/Users/foo', 'C:/Users/foo'],
|
|
['/c:', 'C:/'],
|
|
['/cygdrive/c/Users/foo', 'C:/Users/foo'],
|
|
['/cygdrive/d', 'D:/'],
|
|
['/c/Users/foo', 'C:/Users/foo'],
|
|
['/C/Users/foo', 'C:/Users/foo'],
|
|
['/c/', 'C:/'],
|
|
['/c', 'C:/'],
|
|
];
|
|
|
|
for (const [input, expected] of cases) {
|
|
it(`rewrites "${input}"`, () => {
|
|
const { deps, execFileSync } = stubDeps();
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
expect(bridge.fromShellPath(input)).toBe(expected);
|
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('fromShellPath pass-through', () => {
|
|
it.each(['/dev/null', '/dev/pty0', '/proc/self/status', '/sys/kernel'])(
|
|
'leaves virtual-fs path %s unchanged',
|
|
(input) => {
|
|
const { deps, execFileSync } = stubDeps();
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
expect(bridge.fromShellPath(input)).toBe(input);
|
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
'/',
|
|
'//server/share',
|
|
'//server/share/file.txt',
|
|
'relative/path',
|
|
'relative\\path',
|
|
'file.txt',
|
|
'C:\\Users\\foo',
|
|
'C:/Users/foo',
|
|
'~/Documents',
|
|
])('leaves %s unchanged without consulting cygpath', (input) => {
|
|
const { deps, execFileSync } = stubDeps();
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
expect(bridge.fromShellPath(input)).toBe(input);
|
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('fromShellPath cygpath resolution', () => {
|
|
it('resolves a root-relative path through cygpath and caches per first segment', () => {
|
|
const { deps, execFileSync } = stubDeps({
|
|
existingPaths: [USR_BIN_CYGPATH],
|
|
execFileResults: {
|
|
[cygpathKey('tmp')]: 'C:\\Users\\me\\AppData\\Local\\Temp\\\n',
|
|
},
|
|
});
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
|
|
expect(bridge.fromShellPath('/tmp/scratch/a.txt')).toBe(
|
|
'C:/Users/me/AppData/Local/Temp/scratch/a.txt',
|
|
);
|
|
expect(bridge.fromShellPath('/tmp/other')).toBe('C:/Users/me/AppData/Local/Temp/other');
|
|
expect(bridge.fromShellPath('/tmp')).toBe('C:/Users/me/AppData/Local/Temp');
|
|
expect(execFileSync).toHaveBeenCalledTimes(1);
|
|
expect(execFileSync).toHaveBeenCalledWith(USR_BIN_CYGPATH, [
|
|
'-w',
|
|
'-C',
|
|
'UTF8',
|
|
'--',
|
|
'/tmp',
|
|
]);
|
|
});
|
|
|
|
it('folds dot segments before resolving the mount segment', () => {
|
|
const { deps, execFileSync } = stubDeps({
|
|
existingPaths: [USR_BIN_CYGPATH],
|
|
execFileResults: {
|
|
[cygpathKey('tmp')]: 'C:\\Users\\me\\AppData\\Local\\Temp\n',
|
|
[cygpathKey('home')]: 'C:\\Program Files\\Git\\home\n',
|
|
},
|
|
});
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
|
|
expect(bridge.fromShellPath('/./tmp/note.txt')).toBe(
|
|
'C:/Users/me/AppData/Local/Temp/note.txt',
|
|
);
|
|
expect(bridge.fromShellPath('/../tmp/note.txt')).toBe(
|
|
'C:/Users/me/AppData/Local/Temp/note.txt',
|
|
);
|
|
expect(bridge.fromShellPath('/tmp/../home/x.txt')).toBe('C:/Program Files/Git/home/x.txt');
|
|
expect(execFileSync).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('folds dot segments before lexical drive translation', () => {
|
|
const { deps, execFileSync } = stubDeps();
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
|
|
expect(bridge.fromShellPath('/./c/Projects')).toBe('C:/Projects');
|
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each(['/.', '/..'])('normalizes %s to / without consulting cygpath', (input) => {
|
|
const { deps, execFileSync } = stubDeps();
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
|
|
expect(bridge.fromShellPath(input)).toBe('/');
|
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('resolves a drive-root mount and keeps it absolute', () => {
|
|
const { deps, execFileSync } = stubDeps({
|
|
existingPaths: [USR_BIN_CYGPATH],
|
|
execFileResults: { [cygpathKey('work')]: 'D:\\\n' },
|
|
});
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
|
|
expect(bridge.fromShellPath('/work/app')).toBe('D:/app');
|
|
expect(bridge.fromShellPath('/work')).toBe('D:/');
|
|
expect(execFileSync).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('prefers cygpath.exe next to bash.exe when present', () => {
|
|
const key = `${BIN_CYGPATH} -w -C UTF8 -- /home`;
|
|
const { deps, execFileSync } = stubDeps({
|
|
existingPaths: [BIN_CYGPATH, USR_BIN_CYGPATH],
|
|
execFileResults: { [key]: 'C:\\Users\n' },
|
|
});
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
|
|
expect(bridge.fromShellPath('/home/u/f.txt')).toBe('C:/Users/u/f.txt');
|
|
expect(execFileSync).toHaveBeenCalledTimes(1);
|
|
expect(execFileSync).toHaveBeenCalledWith(BIN_CYGPATH, ['-w', '-C', 'UTF8', '--', '/home']);
|
|
});
|
|
|
|
it('passes through and retries on the next access when cygpath fails', () => {
|
|
const { deps, execFileSync } = stubDeps({
|
|
existingPaths: [USR_BIN_CYGPATH],
|
|
execFileSync: () => {
|
|
throw new Error('cygpath exited 1');
|
|
},
|
|
});
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
|
|
expect(bridge.fromShellPath('/tmp/x')).toBe('/tmp/x');
|
|
expect(bridge.fromShellPath('/tmp/y')).toBe('/tmp/y');
|
|
expect(execFileSync).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('passes through and retries when cygpath output is not an absolute win32 path', () => {
|
|
const { deps, execFileSync } = stubDeps({
|
|
existingPaths: [USR_BIN_CYGPATH],
|
|
execFileResults: { [cygpathKey('tmp')]: 'not a win32 path\n' },
|
|
});
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
|
|
expect(bridge.fromShellPath('/tmp/x')).toBe('/tmp/x');
|
|
expect(bridge.fromShellPath('/tmp/y')).toBe('/tmp/y');
|
|
expect(execFileSync).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('passes through without spawning when cygpath.exe is missing', () => {
|
|
const { deps, execFileSync } = stubDeps();
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
|
|
expect(bridge.fromShellPath('/tmp/x')).toBe('/tmp/x');
|
|
expect(bridge.fromShellPath('/home/u')).toBe('/home/u');
|
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('identity outside win32 bash', () => {
|
|
it('is identity on posix', () => {
|
|
const { deps, execFileSync } = stubDeps();
|
|
const bridge = createShellPathBridge(POSIX_ENV, deps);
|
|
expect(bridge.fromShellPath('/c/Users/foo')).toBe('/c/Users/foo');
|
|
expect(bridge.fromShellPath('/tmp/x')).toBe('/tmp/x');
|
|
expect(bridge.toShellPath('C:\\Users\\foo')).toBe('C:\\Users\\foo');
|
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('is identity on Windows without bash', () => {
|
|
const { deps, execFileSync } = stubDeps();
|
|
const bridge = createShellPathBridge(
|
|
{ osKind: 'Windows', shellName: 'sh', shellPath: 'C:\\sh.exe' },
|
|
deps,
|
|
);
|
|
expect(bridge.fromShellPath('/c/Users/foo')).toBe('/c/Users/foo');
|
|
expect(bridge.toShellPath('C:\\Users\\foo')).toBe('C:\\Users\\foo');
|
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('toShellPath', () => {
|
|
it.each([
|
|
['C:\\Users\\foo', '/c/Users/foo'],
|
|
['C:/Users/foo', '/c/Users/foo'],
|
|
['C:\\', '/c/'],
|
|
['D:\\Projects', '/d/Projects'],
|
|
['\\\\server\\share\\dir', '//server/share/dir'],
|
|
['relative\\path', 'relative/path'],
|
|
['already/posix', 'already/posix'],
|
|
])('maps %s → %s', (input, expected) => {
|
|
const { deps } = stubDeps();
|
|
const bridge = createShellPathBridge(WINDOWS_ENV, deps);
|
|
expect(bridge.toShellPath(input)).toBe(expected);
|
|
});
|
|
});
|