From 32c64734176cc4263dfc933c23f41f1d2e7e33dd Mon Sep 17 00:00:00 2001 From: "J.Jason" <130959319+JJasonSun@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:58:39 +0800 Subject: [PATCH 1/6] docs(cli): document Python invocation on Windows --- docs/install/cli.md | 2 ++ docs/install/troubleshooting.md | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/docs/install/cli.md b/docs/install/cli.md index a36ed58..dcf36a8 100644 --- a/docs/install/cli.md +++ b/docs/install/cli.md @@ -85,3 +85,5 @@ for the canonical command and option reference. When embedding the CLI in a Node application on Windows, follow the [shell-free child-process guidance](./troubleshooting.md#run-call-e-from-node-on-windows). +For Python, see [Windows subprocess guidance](./troubleshooting.md#run-call-e-from-python-on-windows), +including how to avoid `FileNotFoundError: [WinError 2]`. diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md index d11ee31..8a40056 100644 --- a/docs/install/troubleshooting.md +++ b/docs/install/troubleshooting.md @@ -76,6 +76,48 @@ These steps were tested on macOS with Cursor 3.21.16, Node 26.8.2, and CALL-E CL `NODE_USE_ENV_PROXY=1` also resolved the CLI's `fetch failed`. This check does not cover a fresh OAuth login or skill installation. +## Run CALL-E from Python on Windows + +Python's `subprocess.run(["calle", "--help"])` can raise +`FileNotFoundError: [WinError 2]` even when `calle` works in a terminal. +The npm installation provides a `calle.cmd` wrapper on Windows; a direct +process launch does not resolve the bare command like a shell does. This +failure occurs before the CLI starts. + +In the reported Windows test, using `calle.cmd` explicitly displayed help. +For application integrations, use the existing +[CLI launcher](../../packages/cli/docs/cli-reference.md#selecting-the-cli-entry-point) +with a trusted Node executable instead of passing commands through `cmd /c`. +This also avoids the shell argument parsing described below. + +After selecting a trusted CLI installation as described in that guide, +replace these paths with your absolute Node executable and package paths: + +```python +import json +import subprocess +from pathlib import Path + +node = Path(r"C:\Program Files\nodejs\node.exe") +package_dir = Path(r"C:\trusted\node_modules\@call-e\cli") +launcher = package_dir / "scripts" / "run-agent-command.mjs" +request = {"package_dir": str(package_dir), "argv": ["--help"]} + +subprocess.run( + [str(node), str(launcher)], + input=json.dumps(request), + text=True, + shell=False, + check=True, +) +``` + +This help check requires no login and makes no calls. For subsequent commands, +change the request's `argv` array; keep each argument as a separate element +and send the JSON through stdin. Do not construct a shell command from user +text. See the CLI reference for integration attribution and returned argument +arrays. + ## Run CALL-E from Node on Windows ### Symptoms From 4ef424b0d006bb151c016c835eafeefe69131698 Mon Sep 17 00:00:00 2001 From: "J.Jason" <130959319+JJasonSun@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:04:01 +0800 Subject: [PATCH 2/6] docs(cli): keep Windows example aligned with verified workaround --- docs/install/troubleshooting.md | 35 +++++++-------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md index 8a40056..40d38d8 100644 --- a/docs/install/troubleshooting.md +++ b/docs/install/troubleshooting.md @@ -84,39 +84,18 @@ The npm installation provides a `calle.cmd` wrapper on Windows; a direct process launch does not resolve the bare command like a shell does. This failure occurs before the CLI starts. -In the reported Windows test, using `calle.cmd` explicitly displayed help. -For application integrations, use the existing -[CLI launcher](../../packages/cli/docs/cli-reference.md#selecting-the-cli-entry-point) -with a trusted Node executable instead of passing commands through `cmd /c`. -This also avoids the shell argument parsing described below. - -After selecting a trusted CLI installation as described in that guide, -replace these paths with your absolute Node executable and package paths: +In the reported Windows test, specifying `calle.cmd` displayed help: ```python -import json import subprocess -from pathlib import Path - -node = Path(r"C:\Program Files\nodejs\node.exe") -package_dir = Path(r"C:\trusted\node_modules\@call-e\cli") -launcher = package_dir / "scripts" / "run-agent-command.mjs" -request = {"package_dir": str(package_dir), "argv": ["--help"]} - -subprocess.run( - [str(node), str(launcher)], - input=json.dumps(request), - text=True, - shell=False, - check=True, -) + +subprocess.run(["calle.cmd", "--help"]) ``` -This help check requires no login and makes no calls. For subsequent commands, -change the request's `argv` array; keep each argument as a separate element -and send the JSON through stdin. Do not construct a shell command from user -text. See the CLI reference for integration attribution and returned argument -arrays. +This help check requires no login and makes no calls. The test confirms this +invocation; it does not validate arbitrary arguments through the `.cmd` wrapper. +For application integrations, see the existing +[CLI launcher guidance](../../packages/cli/docs/cli-reference.md#selecting-the-cli-entry-point). ## Run CALL-E from Node on Windows From 4c68ec21d9a2706c81275ac3cf45486148d512aa Mon Sep 17 00:00:00 2001 From: "J.Jason" <130959319+JJasonSun@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:08:43 +0800 Subject: [PATCH 3/6] docs(cli): recommend the Python launcher and explain Windows pitfalls --- docs/install/troubleshooting.md | 78 ++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md index 40d38d8..2593d09 100644 --- a/docs/install/troubleshooting.md +++ b/docs/install/troubleshooting.md @@ -78,24 +78,78 @@ cover a fresh OAuth login or skill installation. ## Run CALL-E from Python on Windows -Python's `subprocess.run(["calle", "--help"])` can raise -`FileNotFoundError: [WinError 2]` even when `calle` works in a terminal. -The npm installation provides a `calle.cmd` wrapper on Windows; a direct -process launch does not resolve the bare command like a shell does. This -failure occurs before the CLI starts. +For Python applications, invoke the existing CLI launcher with a trusted Node +executable and pass arguments as JSON through stdin. This avoids Windows +command-name lookup and shell parsing of argument values. The launcher verifies +that the selected package is `@call-e/cli` before running it. + +### Recommended invocation -In the reported Windows test, specifying `calle.cmd` displayed help: +Follow [CLI entry point selection](../../packages/cli/docs/cli-reference.md#selecting-the-cli-entry-point) +to select a trusted installation. Replace the two paths below with your absolute +Node executable and `@call-e/cli` package paths: ```python +import json import subprocess - -subprocess.run(["calle.cmd", "--help"]) +from pathlib import Path + +node = Path(r"C:\Program Files\nodejs\node.exe") +package_dir = Path(r"C:\trusted\node_modules\@call-e\cli") +launcher = package_dir / "scripts" / "run-agent-command.mjs" +request = {"package_dir": str(package_dir), "argv": ["--help"]} + +subprocess.run( + [str(node), str(launcher)], + input=json.dumps(request), + text=True, + shell=False, + check=True, +) ``` -This help check requires no login and makes no calls. The test confirms this -invocation; it does not validate arbitrary arguments through the `.cmd` wrapper. -For application integrations, see the existing -[CLI launcher guidance](../../packages/cli/docs/cli-reference.md#selecting-the-cli-entry-point). +This help check requires no login and makes no calls. Use the same invocation +on macOS or Linux with the corresponding absolute paths. For other commands, +change `argv`; see the CLI reference for integration attribution and returned +argument arrays. + +### Common questions + +**Why does `calle` work in my terminal but fail from Python?** + +Python's `subprocess.run(["calle", "--help"])` can raise +`FileNotFoundError: [WinError 2]` on Windows. npm provides a `calle.cmd` wrapper, +while a direct process launch does not resolve the bare name like a shell does. +The CLI has not started when this error occurs. + +**Can I use `calle.cmd` or `cmd /c` instead?** + +The reported Windows test confirmed that `calle.cmd --help` displayed help when +called from Python, and the reporter used `cmd /c` for a status query. Those +results do not verify arbitrary arguments. For application integrations, use +the recommended launcher: command scripts can reinterpret quotes, special +characters, and multiline text. Adding `shell=True` is not needed. + +**Where do I find the paths?** + +In PowerShell, `Get-Command node.exe` shows the Node executable. For a global +npm installation, `npm root -g` gives the package root; append `@call-e/cli` to +that directory. For a local installation, select the trusted application's +`node_modules/@call-e/cli` directory. Configure these paths once instead of +running discovery commands before every invocation. + +**How do I pass spaces, quotes, or multiline text?** + +Keep each argument as one string in `argv`, including an entire multiline value +as one element. Let `json.dumps` serialize the request. Do not add shell quotes +to individual arguments, concatenate a command string, or use `shell=True`. + +**What if the launcher rejects the package or entry point?** + +Check that `package_dir` names the `@call-e/cli` directory, not its parent +`node_modules` directory or the older `@call-e/calle` SDK. If the launcher reports +that command-help checks failed, update the trusted CLI installation as directed +by the CLI reference before authenticating. ## Run CALL-E from Node on Windows From 00bcdd04bdfdb46c53cd847b08283d14ddf5e8e7 Mon Sep 17 00:00:00 2001 From: "J.Jason" <130959319+JJasonSun@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:16:16 +0800 Subject: [PATCH 4/6] fix(examples): resolve batch runner commands on Windows --- .github/workflows/ci.yml | 11 ++++ examples/python-batch-runner/README.md | 7 +++ examples/python-batch-runner/client.py | 25 +++++++- examples/python-batch-runner/test_command.py | 65 ++++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 examples/python-batch-runner/test_command.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc4f5c4..1f082d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,9 @@ jobs: - name: Test Python OAuth example run: uv run --isolated --project examples/mcp-oauth-client/python pytest examples/mcp-oauth-client/python + - name: Test Python batch runner + run: uv run --isolated --project examples/python-batch-runner pytest examples/python-batch-runner + - name: Validate package contents run: pnpm pack:dry-run @@ -71,3 +74,11 @@ jobs: - name: Check integration versions run: pnpm check:versions + + - name: Set up uv + uses: astral-sh/setup-uv@v10.0.1 + with: + python-version: "3.12" + + - name: Test Python command launching on Windows + run: uv run --isolated --project examples/python-batch-runner pytest examples/python-batch-runner/test_command.py diff --git a/examples/python-batch-runner/README.md b/examples/python-batch-runner/README.md index 6b1d14b..ca44b0c 100644 --- a/examples/python-batch-runner/README.md +++ b/examples/python-batch-runner/README.md @@ -25,6 +25,13 @@ The script runs these prechecks before processing the JSONL file: 4. If the CLI is not logged in, pause and ask the operator to run `calle auth login`, then press Enter to continue. +On Windows, the runner resolves `calle` to its installed executable (normally +`calle.cmd`) before checking authentication. `--calle-command` also accepts an +executable path; quote paths containing spaces when supplying a command with +additional arguments. This CLI subprocess only checks authentication; call +payloads are sent directly through MCP. For general Python-to-CLI integrations, +see [Windows invocation guidance](../../docs/install/troubleshooting.md#run-call-e-from-python-on-windows). + Input is JSONL. Each line may use the customer payload shape directly: ```json diff --git a/examples/python-batch-runner/client.py b/examples/python-batch-runner/client.py index 84a4b4b..9c6c3c2 100644 --- a/examples/python-batch-runner/client.py +++ b/examples/python-batch-runner/client.py @@ -129,6 +129,21 @@ def parse_positive_float(value: str) -> float: return parsed +def parse_cli_command(value: str) -> list[str]: + # An executable path may contain spaces without containing CLI arguments. + if Path(value).expanduser().is_file(): + return [str(Path(value).expanduser())] + if sys.platform == "win32": + # Preserve Windows path separators; quotes group paths containing spaces. + parts = shlex.split(value, posix=False) + parts = [part[1:-1] if part.startswith('"') and part.endswith('"') else part for part in parts] + else: + parts = shlex.split(value) + if not parts: + raise argparse.ArgumentTypeError("expected a CLI command or executable path") + return parts + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Run CALL-E MCP calls from a JSONL batch file.") parser.add_argument("--input", required=True, type=Path, help="Path to the input JSONL file.") @@ -142,7 +157,7 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument("--channel", default=DEFAULT_CHANNEL, help=f"MCP channel. Default: {DEFAULT_CHANNEL}.") parser.add_argument("--server-url", help="Full MCP server URL. Overrides --base-url and --channel.") parser.add_argument("--cache-root", default=DEFAULT_CACHE_ROOT, help=f"calle CLI cache root. Default: {DEFAULT_CACHE_ROOT}.") - parser.add_argument("--calle-command", default="calle", help="calle CLI command or path. Default: calle.") + parser.add_argument("--calle-command", type=parse_cli_command, default="calle", help="calle CLI command or path. Default: calle.") parser.add_argument("--npm-command", default="npm", help="npm command used for automatic CLI installation. Default: npm.") parser.add_argument("--cli-package", default=DEFAULT_CLI_PACKAGE, help=f"CLI package to install when calle is missing. Default: {DEFAULT_CLI_PACKAGE}.") parser.add_argument("--no-auto-install-cli", action="store_true", help="Fail if calle is missing instead of installing it.") @@ -168,7 +183,7 @@ def read_config(argv: list[str] | None = None) -> Config: channel=args.channel, server_url=resolve_server_url(args.base_url, args.channel, args.server_url), cache_root=args.cache_root, - calle_command=shlex.split(args.calle_command), + calle_command=args.calle_command, npm_command=args.npm_command, cli_package=args.cli_package, auto_install_cli=not args.no_auto_install_cli, @@ -181,8 +196,12 @@ def read_config(argv: list[str] | None = None) -> Config: def run_command(command: list[str], *, capture: bool = True) -> subprocess.CompletedProcess[str]: + executable = str(Path(command[0]).expanduser()) + resolved = shutil.which(executable) + if resolved is None: + raise CliUnavailableError(f"Executable not found: {command[0]}") return subprocess.run( - command, + [resolved, *command[1:]], check=False, text=True, capture_output=capture, diff --git a/examples/python-batch-runner/test_command.py b/examples/python-batch-runner/test_command.py new file mode 100644 index 0000000..bbe4c34 --- /dev/null +++ b/examples/python-batch-runner/test_command.py @@ -0,0 +1,65 @@ +import json +import os +import sys +from unittest.mock import patch + +import pytest + +import client + + +def test_windows_command_paths_preserve_backslashes(): + with patch.object(client.sys, "platform", "win32"): + assert client.parse_cli_command(r"C:\tools\calle.cmd") == [r"C:\tools\calle.cmd"] + assert client.parse_cli_command(r'"C:\Program Files\nodejs\node.exe" "C:\tools\calle.js"') == [ + r"C:\Program Files\nodejs\node.exe", r"C:\tools\calle.js" + ] + + +def test_existing_executable_path_with_spaces(tmp_path): + executable = tmp_path / "my cli" + executable.touch() + assert client.parse_cli_command(str(executable)) == [str(executable)] + + +def test_launch_uses_resolved_command_and_preserves_arguments(): + with patch.object(client.shutil, "which", return_value=r"C:\npm\calle.cmd"), patch.object(client.subprocess, "run") as run: + assert client.executable_exists(["calle"]) + client.run_command(["calle", "auth", "status", "--cache-root", "path with spaces"]) + assert run.call_args.args[0] == [r"C:\npm\calle.cmd", "auth", "status", "--cache-root", "path with spaces"] + assert run.call_args.kwargs.get("shell", False) is False + + +def test_missing_command_has_actionable_error(): + with patch.object(client.shutil, "which", return_value=None): + with pytest.raises(client.CliUnavailableError, match="Executable not found"): + client.run_command(["missing-calle", "--help"]) + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows npm shim lookup") +def test_windows_cmd_lookup_and_launch(tmp_path, monkeypatch): + shim = tmp_path / "calle.cmd" + shim.write_text('@echo off\necho {"usable":true}\n') + monkeypatch.setenv("PATH", str(tmp_path) + os.pathsep + os.environ["PATH"]) + assert client.executable_exists(["calle"]) + result = client.run_command(["calle", "auth", "status", "--json"]) + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == {"usable": True} + + +def test_documented_python_launcher(): + import re + import shutil + import subprocess + from pathlib import Path + + root = Path(__file__).resolve().parents[2] + section = (root / "docs/install/troubleshooting.md").read_text().split( + "## Run CALL-E from Python on Windows", 1 + )[1].split("## Run CALL-E from Node on Windows", 1)[0] + code = re.search(r"```python\n(.*?)```", section, re.S).group(1) + code = code.replace(r"C:\Program Files\nodejs\node.exe", shutil.which("node")) + code = code.replace(r"C:\trusted\node_modules\@call-e\cli", str(root / "packages/cli")) + result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) + assert result.returncode == 0, result.stderr + assert "Usage: calle" in result.stdout From b127e87f88ac2ced9fdf0dcd16cc48525737110b Mon Sep 17 00:00:00 2001 From: "J.Jason" <130959319+JJasonSun@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:26:28 +0800 Subject: [PATCH 5/6] fix(examples): unify executable lookup and preserve explicit paths --- examples/README.md | 10 +++++++ examples/python-batch-runner/README.md | 20 +++++++++---- examples/python-batch-runner/client.py | 20 ++++++------- examples/python-batch-runner/test_command.py | 31 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/examples/README.md b/examples/README.md index fb37493..1ee1eb2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,3 +12,13 @@ not define a supported application API. The default e2e tests use a local fake broker/OAuth/MCP server, so they do not require real CALL-E credentials or browser login. Live verification against the real CALL-E service is opt-in through each example README. + +## Invocation conventions + +The OAuth and broker examples connect directly to MCP; they do not require a +CLI subprocess. The batch runner reuses the CLI login state before making MCP +calls. For applications that invoke CLI commands, follow the shared +[Python/Windows invocation guide](../docs/install/troubleshooting.md#run-call-e-from-python-on-windows) +and [CLI reference](../packages/cli/docs/cli-reference.md#selecting-the-cli-entry-point) +instead of constructing shell command strings. Keep platform troubleshooting +in the guide and example-specific options in each example README. diff --git a/examples/python-batch-runner/README.md b/examples/python-batch-runner/README.md index ca44b0c..8b87e55 100644 --- a/examples/python-batch-runner/README.md +++ b/examples/python-batch-runner/README.md @@ -25,12 +25,20 @@ The script runs these prechecks before processing the JSONL file: 4. If the CLI is not logged in, pause and ask the operator to run `calle auth login`, then press Enter to continue. -On Windows, the runner resolves `calle` to its installed executable (normally -`calle.cmd`) before checking authentication. `--calle-command` also accepts an -executable path; quote paths containing spaces when supplying a command with -additional arguments. This CLI subprocess only checks authentication; call -payloads are sent directly through MCP. For general Python-to-CLI integrations, -see [Windows invocation guidance](../../docs/install/troubleshooting.md#run-call-e-from-python-on-windows). +## CLI invocation + +The runner uses the same executable lookup for its CLI precheck, authentication +check, and npm installation. Bare names such as `calle` use PATH (including the +Windows `calle.cmd` wrapper); explicit paths such as `./calle` or an absolute +path keep their meaning. An existing file must also be executable. + +`--calle-command` accepts an executable path or a command with arguments. Use +double quotes around paths containing spaces when combining them with arguments. +The runner passes arguments separately without enabling a shell. Its CLI calls +only check authentication; call payloads go directly through MCP. + +For general Python-to-CLI integrations and Windows troubleshooting, follow the +[shared CLI invocation guidance](../../docs/install/troubleshooting.md#run-call-e-from-python-on-windows). Input is JSONL. Each line may use the customer payload shape directly: diff --git a/examples/python-batch-runner/client.py b/examples/python-batch-runner/client.py index 9c6c3c2..596c780 100644 --- a/examples/python-batch-runner/client.py +++ b/examples/python-batch-runner/client.py @@ -2,6 +2,7 @@ import asyncio import hashlib import json +import os import re import shlex import shutil @@ -132,7 +133,7 @@ def parse_positive_float(value: str) -> float: def parse_cli_command(value: str) -> list[str]: # An executable path may contain spaces without containing CLI arguments. if Path(value).expanduser().is_file(): - return [str(Path(value).expanduser())] + return [value] if sys.platform == "win32": # Preserve Windows path separators; quotes group paths containing spaces. parts = shlex.split(value, posix=False) @@ -195,9 +196,13 @@ def read_config(argv: list[str] | None = None) -> Config: ) +def resolve_executable(executable: str) -> str | None: + # Keep explicit relative paths (./tool) distinct from PATH command names. + return shutil.which(os.path.expanduser(executable)) + + def run_command(command: list[str], *, capture: bool = True) -> subprocess.CompletedProcess[str]: - executable = str(Path(command[0]).expanduser()) - resolved = shutil.which(executable) + resolved = resolve_executable(command[0]) if resolved is None: raise CliUnavailableError(f"Executable not found: {command[0]}") return subprocess.run( @@ -209,16 +214,11 @@ def run_command(command: list[str], *, capture: bool = True) -> subprocess.Compl def executable_exists(command: list[str]) -> bool: - if not command: - return False - executable = command[0] - if Path(executable).expanduser().exists(): - return True - return shutil.which(executable) is not None + return bool(command) and resolve_executable(command[0]) is not None def install_calle_cli(config: Config, console: Console) -> None: - npm_path = shutil.which(config.npm_command) + npm_path = resolve_executable(config.npm_command) if not npm_path: raise CliUnavailableError( f"`{config.calle_command[0]}` is not installed and `{config.npm_command}` was not found. " diff --git a/examples/python-batch-runner/test_command.py b/examples/python-batch-runner/test_command.py index bbe4c34..b8e27f3 100644 --- a/examples/python-batch-runner/test_command.py +++ b/examples/python-batch-runner/test_command.py @@ -63,3 +63,34 @@ def test_documented_python_launcher(): result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert "Usage: calle" in result.stdout + + +def test_explicit_relative_path_does_not_select_path_namesake(tmp_path, monkeypatch): + name = "local cli.cmd" if sys.platform == "win32" else "local cli" + local = tmp_path / name + shadow_dir = tmp_path / "path-bin" + shadow_dir.mkdir() + for path, output in [(local, "local"), (shadow_dir / name, "shadow")]: + prefix = "@echo off\n" if sys.platform == "win32" else "#!/bin/sh\n" + path.write_text(prefix + f"echo {output}\n") + path.chmod(0o755) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("PATH", str(shadow_dir) + os.pathsep + os.environ["PATH"]) + explicit = "./" + name + for value in [explicit, f'"{explicit}" --help']: + command = client.parse_cli_command(value) + assert client.executable_exists(command) + result = client.run_command(command) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "local" + assert client.run_command([name]).stdout.strip() == "shadow" + + +def test_non_executable_file_fails_precheck_on_posix(tmp_path): + if sys.platform == "win32": + pytest.skip("POSIX executable permission") + path = tmp_path / "not-executable" + path.touch(mode=0o600) + assert not client.executable_exists([str(path)]) + with pytest.raises(client.CliUnavailableError, match="Executable not found"): + client.run_command([str(path)]) From 6ab313d0d69abb916c12318e637225d8ae19f620 Mon Sep 17 00:00:00 2001 From: "J.Jason" <130959319+JJasonSun@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:26:48 +0800 Subject: [PATCH 6/6] test(examples): respect Windows bare command search order --- examples/python-batch-runner/test_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/python-batch-runner/test_command.py b/examples/python-batch-runner/test_command.py index b8e27f3..9d6a671 100644 --- a/examples/python-batch-runner/test_command.py +++ b/examples/python-batch-runner/test_command.py @@ -83,7 +83,9 @@ def test_explicit_relative_path_does_not_select_path_namesake(tmp_path, monkeypa result = client.run_command(command) assert result.returncode == 0, result.stderr assert result.stdout.strip() == "local" - assert client.run_command([name]).stdout.strip() == "shadow" + # Windows may also search the current directory for a bare command. + if sys.platform != "win32": + assert client.run_command([name]).stdout.strip() == "shadow" def test_non_executable_file_fails_precheck_on_posix(tmp_path):