diff --git a/.changeset/v2-msys2-bash-detection.md b/.changeset/v2-msys2-bash-detection.md new file mode 100644 index 000000000..323a80f82 --- /dev/null +++ b/.changeset/v2-msys2-bash-detection.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix bash auto-detection on Windows in the experimental v2 engine when git comes from a native MSYS2 toolchain (ucrt64/clang64/clangarm64). diff --git a/packages/agent-core-v2/src/_base/execEnv/environmentProbe.ts b/packages/agent-core-v2/src/_base/execEnv/environmentProbe.ts index f813c29d4..4756e0358 100644 --- a/packages/agent-core-v2/src/_base/execEnv/environmentProbe.ts +++ b/packages/agent-core-v2/src/_base/execEnv/environmentProbe.ts @@ -7,10 +7,10 @@ * same suite runs identically on any host OS. `probeHostEnvironmentFromNode()` * bundles the Node defaults for production callers and memoises the promise. * - * On Windows the probe expects Git Bash (the canonical POSIX shell that ships - * with Git for Windows). If it cannot be located the function throws a plain - * `Error` with the checked paths in the message; the App-scope host-environment - * service catches that at first resolution. Set `KIMI_SHELL_PATH` to override. + * On Windows the probe expects bash from Git for Windows or MSYS2. If it + * cannot be located the function throws a plain `Error` with the checked paths + * in the message; the App-scope host-environment service catches that at first + * resolution. Set `KIMI_SHELL_PATH` to override. * * Vendored from `@moonshot-ai/kaos` `environment.ts` — kept as a pure helper * with no DI dependencies. @@ -57,6 +57,14 @@ export interface HostEnvironmentProbeDeps { const GIT_EXEC_PATH_TIMEOUT_MS = 5_000; +const MINGW_PREFIX_SET: ReadonlySet = new Set([ + 'mingw32', + 'mingw64', + 'ucrt64', + 'clang64', + 'clangarm64', +]); + function resolveOsKind(platform: string): OsKind { switch (platform) { case 'darwin': @@ -222,7 +230,7 @@ function gitBashCandidatesFromGitExecPath(execPath: string): readonly string[] { const parts = normalized.split('\\'); for (let i = parts.length - 1; i >= 0; i -= 1) { const segment = parts[i]?.toLowerCase(); - if (segment === 'mingw32' || segment === 'mingw64') { + if (segment !== undefined && MINGW_PREFIX_SET.has(segment)) { const root = parts.slice(0, i).join('\\'); if (root.length > 0) { return gitBashCandidatesFromGitRoot(root); diff --git a/packages/agent-core-v2/test/_base/execEnv/environmentProbe.test.ts b/packages/agent-core-v2/test/_base/execEnv/environmentProbe.test.ts new file mode 100644 index 000000000..497f5792d --- /dev/null +++ b/packages/agent-core-v2/test/_base/execEnv/environmentProbe.test.ts @@ -0,0 +1,100 @@ +/** + * Host environment probe — MSYS2 bash detection. + * + * Pins the Windows shell probe against native MSYS2 toolchains: a git whose + * `git --exec-path` reports an `ucrt64` / `clang64` / `clangarm64` prefix + * (e.g. `C:/msys64/ucrt64/libexec/git-core`) must walk back to the MSYS2 root + * and resolve the shared bash at `usr\bin\bash.exe`, instead of failing to + * detect any shell. + * + * All tests expect `probeHostEnvironment()` to be a pure function of injected + * platform probes (no ambient state) so the same suite runs identically on + * macOS/Linux/Windows CI runners. + * + * Ported from `packages/kaos/test/environment.test.ts` (the MSYS2 cases added + * by the bash-detection fix); the v1 file carries the full POSIX / Git for + * Windows / Scoop shim matrix, which the vendored probe shares verbatim. + */ + +import { describe, expect, it } from 'vitest'; + +import { + probeHostEnvironment, + type HostEnvironmentProbeDeps, +} from '#/_base/execEnv/environmentProbe'; + +interface StubOpts { + readonly platform: string; + readonly env?: Record; + readonly existingPaths?: readonly string[]; + readonly execFileResults?: Readonly>; +} + +/** Build a stub deps bag mimicking Node's `os` + `process` surface. */ +function stubDeps(opts: StubOpts): HostEnvironmentProbeDeps { + const existing = new Set(opts.existingPaths ?? []); + return { + platform: opts.platform, + arch: 'x86_64', + release: '1.2.3', + homeDir: 'C:\\Users\\me', + env: opts.env ?? {}, + isFile: async (path: string) => existing.has(path), + execFileText: async (file: string, args: readonly string[]) => + opts.execFileResults?.[execFileKey(file, args)], + }; +} + +function execFileKey(file: string, args: readonly string[]): string { + return [file, ...args].join('\0'); +} + +describe('probeHostEnvironment', () => { + it('resolves MSYS2 ucrt64 native git through git --exec-path', async () => { + const gitExe = 'C:\\msys64\\ucrt64\\bin\\git.exe'; + const env = await probeHostEnvironment( + stubDeps({ + platform: 'win32', + env: { PATH: 'C:\\msys64\\ucrt64\\bin' }, + execFileResults: { + [execFileKey(gitExe, ['--exec-path'])]: 'C:/msys64/ucrt64/libexec/git-core\n', + }, + existingPaths: [gitExe, 'C:\\msys64\\usr\\bin\\bash.exe'], + }), + ); + expect(env.shellName).toBe('bash'); + expect(env.shellPath).toBe('C:\\msys64\\usr\\bin\\bash.exe'); + }); + + it('resolves MSYS2 clang64 native git through git --exec-path', async () => { + const gitExe = 'C:\\msys64\\clang64\\bin\\git.exe'; + const env = await probeHostEnvironment( + stubDeps({ + platform: 'win32', + env: { PATH: 'C:\\msys64\\clang64\\bin' }, + execFileResults: { + [execFileKey(gitExe, ['--exec-path'])]: 'C:/msys64/clang64/libexec/git-core\n', + }, + existingPaths: [gitExe, 'C:\\msys64\\usr\\bin\\bash.exe'], + }), + ); + expect(env.shellName).toBe('bash'); + expect(env.shellPath).toBe('C:\\msys64\\usr\\bin\\bash.exe'); + }); + + it('resolves MSYS2 clangarm64 native git through git --exec-path', async () => { + const gitExe = 'C:\\msys64\\clangarm64\\bin\\git.exe'; + const env = await probeHostEnvironment( + stubDeps({ + platform: 'win32', + env: { PATH: 'C:\\msys64\\clangarm64\\bin' }, + execFileResults: { + [execFileKey(gitExe, ['--exec-path'])]: 'C:/msys64/clangarm64/libexec/git-core\n', + }, + existingPaths: [gitExe, 'C:\\msys64\\usr\\bin\\bash.exe'], + }), + ); + expect(env.shellName).toBe('bash'); + expect(env.shellPath).toBe('C:\\msys64\\usr\\bin\\bash.exe'); + }); +});