mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(mcp): honor Windows PATH key casing in doctor (#121595)
This commit is contained in:
committed by
GitHub
parent
01884775aa
commit
63776cf5a0
+1
-1
@@ -1961,7 +1961,7 @@
|
||||
"test:unit:fast:audit": "node --import tsx scripts/test-unit-fast-audit.mts",
|
||||
"test:voicecall:closedloop": "node --import tsx scripts/test-voicecall-closedloop.mts",
|
||||
"test:watch": "node --import tsx scripts/test-projects.mts --watch",
|
||||
"test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/utils.test.ts src/commands/agents.commands.list.test.ts src/cli/daemon-cli/status.print.test.ts packages/terminal-core/src/display-string.test.ts src/agents/sandbox/fs-paths.test.ts src/agents/sessions/tools/render-utils.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/process-env.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/utils.test.ts src/commands/agents.commands.list.test.ts src/cli/daemon-cli/status.print.test.ts src/cli/mcp-cli.path-case.windows.test.ts packages/terminal-core/src/display-string.test.ts src/agents/sandbox/fs-paths.test.ts src/agents/sessions/tools/render-utils.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"test:windows:schtasks:integration": "node --import tsx scripts/run-with-env.mts CI_WINDOWS_SCHTASKS_INTEGRATION=1 OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs src/daemon/schtasks.integration.e2e.test.ts",
|
||||
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
|
||||
"tool-display:write": "node --import tsx scripts/tool-display.ts --write",
|
||||
|
||||
@@ -73,6 +73,8 @@ const WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE =
|
||||
/^src\/media-understanding\/attachments\.(?:cache(?:\.test)?|file-url\.windows\.test|normalize(?:\.test)?)\.ts$/;
|
||||
const WINDOWS_HOME_DISPLAY_SCOPE_RE =
|
||||
/^(?:src\/(?:utils(?:\.test)?|infra\/(?:home-display|path-guards)|commands\/agents\.commands\.list(?:\.test)?|cli\/daemon-cli\/status\.print(?:\.test)?|agents\/(?:sandbox\/fs-paths|sessions\/tools\/render-utils)(?:\.test)?)|packages\/terminal-core\/src\/display-string(?:\.test)?)\.ts$/;
|
||||
const WINDOWS_MCP_ENV_SCOPE_RE =
|
||||
/^src\/(?:cli\/mcp-cli(?:\.path-case\.windows)?\.test|cli\/mcp-cli|infra\/process-env(?:\.test)?)\.ts$/;
|
||||
const CONTROL_UI_I18N_SCOPE_RE =
|
||||
/^(ui\/src\/i18n\/|ui\/config\/control-ui-locales\.ts$|scripts\/(?:control-ui-i18n(?:-verify)?\.ts|lib\/control-ui-i18n-(?:catalog|config|raw-copy|sync-plan)\.ts)$|\.github\/workflows\/control-ui-locale-refresh\.yml$)/;
|
||||
const CONTROL_UI_RAW_COPY_SOURCE_RE = /^ui\/src\/(?:app|components|lib|pages)\/.*\.tsx?$/;
|
||||
@@ -183,7 +185,8 @@ export function detectChangedScope(changedPaths) {
|
||||
WINDOWS_DAEMON_SCOPE_RE.test(path) ||
|
||||
WINDOWS_USAGE_TEMPLATE_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE.test(path) ||
|
||||
WINDOWS_HOME_DISPLAY_SCOPE_RE.test(path)) &&
|
||||
WINDOWS_HOME_DISPLAY_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MCP_ENV_SCOPE_RE.test(path)) &&
|
||||
(!facts.isTestOnly ||
|
||||
WINDOWS_TEST_SCOPE_RE.test(path) ||
|
||||
WINDOWS_FILE_URL_SCOPE_RE.test(path) ||
|
||||
@@ -191,7 +194,8 @@ export function detectChangedScope(changedPaths) {
|
||||
WINDOWS_DAEMON_SCOPE_RE.test(path) ||
|
||||
WINDOWS_USAGE_TEMPLATE_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE.test(path) ||
|
||||
WINDOWS_HOME_DISPLAY_SCOPE_RE.test(path))
|
||||
WINDOWS_HOME_DISPLAY_SCOPE_RE.test(path) ||
|
||||
WINDOWS_MCP_ENV_SCOPE_RE.test(path))
|
||||
) {
|
||||
runWindows = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome } from "../config/home-env.test-harness.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import {
|
||||
cleanupMcpCliTestState,
|
||||
createWorkspace,
|
||||
lastLogLine,
|
||||
mockLog,
|
||||
resetMcpCliTestState,
|
||||
runMcpCommand,
|
||||
} from "./mcp-cli.test-harness.js";
|
||||
|
||||
describe.runIf(process.platform === "win32")("MCP doctor Windows PATH casing", () => {
|
||||
beforeEach(() => {
|
||||
resetMcpCliTestState();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupMcpCliTestState();
|
||||
});
|
||||
|
||||
it("finds a real stdio command through an arbitrarily cased configured PATH", async () => {
|
||||
await withTempHome("openclaw-cli-mcp-path-case-home-", async () => {
|
||||
try {
|
||||
const workspaceDir = await createWorkspace();
|
||||
const binDir = path.join(workspaceDir, "Mixed Case Bin");
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
await fs.writeFile(path.join(binDir, "DOCS-MCP.CMD"), "@echo off\r\nexit /b 0\r\n", "utf8");
|
||||
vi.spyOn(process, "cwd").mockReturnValue(workspaceDir);
|
||||
|
||||
await runMcpCommand([
|
||||
"mcp",
|
||||
"set",
|
||||
"docs",
|
||||
JSON.stringify({ command: "docs-mcp", env: { pAtH: binDir } }),
|
||||
]);
|
||||
mockLog.mockClear();
|
||||
|
||||
await runMcpCommand(["mcp", "doctor", "--json"]);
|
||||
|
||||
expect(JSON.parse(lastLogLine())).toMatchObject({
|
||||
ok: true,
|
||||
servers: [{ name: "docs", ok: true, issues: [] }],
|
||||
});
|
||||
} finally {
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -560,6 +560,47 @@ describe("mcp cli", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"does not treat Path as PATH when checking MCP stdio commands",
|
||||
async () => {
|
||||
await withTempHome("openclaw-cli-mcp-home-", async () => {
|
||||
const workspaceDir = await createWorkspace();
|
||||
const binDir = path.join(workspaceDir, "bin");
|
||||
const commandPath = path.join(binDir, "mis-cased-path-mcp");
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
await fs.writeFile(commandPath, "#!/bin/sh\nexit 0\n", "utf-8");
|
||||
await fs.chmod(commandPath, 0o755);
|
||||
vi.spyOn(process, "cwd").mockReturnValue(workspaceDir);
|
||||
|
||||
await runMcpCommand([
|
||||
"mcp",
|
||||
"set",
|
||||
"docs",
|
||||
JSON.stringify({ command: "mis-cased-path-mcp", env: { Path: binDir } }),
|
||||
]);
|
||||
mockLog.mockClear();
|
||||
|
||||
await expect(runMcpCommand(["mcp", "doctor", "--json"])).rejects.toThrow("__exit__:1");
|
||||
|
||||
expect(JSON.parse(lastLogLine())).toMatchObject({
|
||||
ok: false,
|
||||
servers: [
|
||||
{
|
||||
name: "docs",
|
||||
ok: false,
|
||||
issues: [
|
||||
{
|
||||
level: "error",
|
||||
message: "stdio command not found or not executable: mis-cased-path-mcp",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("resolves relative configured PATH entries from the MCP stdio cwd", async () => {
|
||||
await withTempHome("openclaw-cli-mcp-home-", async () => {
|
||||
const workspaceDir = await createWorkspace();
|
||||
|
||||
+4
-14
@@ -39,6 +39,7 @@ import {
|
||||
startOAuthLoopbackCallbackServer,
|
||||
type OAuthLoopbackCallbackServer,
|
||||
} from "../infra/oauth-loopback-callback.js";
|
||||
import { resolveEnvironmentValue } from "../infra/process-env.js";
|
||||
import { serveOpenClawChannelMcp } from "../mcp/channel-server.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { runTasksWithConcurrency } from "../utils/run-with-concurrency.js";
|
||||
@@ -267,19 +268,6 @@ function executableCandidates(command: string): string[] {
|
||||
return [command, ...extensions.map((extension) => `${command}${extension.toLowerCase()}`)];
|
||||
}
|
||||
|
||||
function resolveEffectivePath(env: Record<string, string> | undefined): string {
|
||||
if (!env) {
|
||||
return process.env.PATH ?? "";
|
||||
}
|
||||
if (typeof env.PATH === "string") {
|
||||
return env.PATH;
|
||||
}
|
||||
if (process.platform === "win32" && typeof env.Path === "string") {
|
||||
return env.Path;
|
||||
}
|
||||
return process.env.PATH ?? "";
|
||||
}
|
||||
|
||||
async function commandExists(
|
||||
command: string,
|
||||
cwd: unknown,
|
||||
@@ -290,7 +278,9 @@ async function commandExists(
|
||||
if (hasPathSeparator) {
|
||||
return isExecutable(resolveConfiguredPath(command, cwd));
|
||||
}
|
||||
const pathEntries = resolveEffectivePath(env)
|
||||
const configuredPath =
|
||||
process.platform === "win32" ? resolveEnvironmentValue(env, "PATH") : env?.PATH;
|
||||
const pathEntries = (configuredPath ?? process.env.PATH ?? "")
|
||||
.split(path.delimiter)
|
||||
.map((entry) => entry.trim() || ".");
|
||||
for (const pathEntry of pathEntries) {
|
||||
|
||||
@@ -201,6 +201,21 @@ describe("detectChangedScope Windows routing", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("routes MCP environment resolution and native doctor coverage to Windows", () => {
|
||||
for (const mcpPath of [
|
||||
"src/cli/mcp-cli.ts",
|
||||
"src/cli/mcp-cli.test.ts",
|
||||
"src/cli/mcp-cli.path-case.windows.test.ts",
|
||||
"src/infra/process-env.ts",
|
||||
"src/infra/process-env.test.ts",
|
||||
]) {
|
||||
expect(detectChangedScope([mcpPath]), mcpPath).toMatchObject({
|
||||
runNode: true,
|
||||
runWindows: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("routes SecretRef path-security changes and native fixtures to Windows", () => {
|
||||
for (const secretRefPath of [
|
||||
"src/commands/doctor-gateway-auth-token.ts",
|
||||
|
||||
@@ -355,4 +355,11 @@ describe("package scripts", () => {
|
||||
expect(script).toContain("src/agents/sandbox/fs-paths.test.ts");
|
||||
expect(script).toContain("src/agents/sessions/tools/render-utils.test.ts");
|
||||
});
|
||||
|
||||
it("runs MCP environment and native doctor coverage in Windows CI", () => {
|
||||
const script = readPackageJson().scripts["test:windows:ci"];
|
||||
|
||||
expect(script).toContain("src/infra/process-env.test.ts");
|
||||
expect(script).toContain("src/cli/mcp-cli.path-case.windows.test.ts");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user