diff --git a/tests/test_package_runtime_assets_contract.sh b/tests/test_package_runtime_assets_contract.sh index 04446f82c..8ef4a36c8 100644 --- a/tests/test_package_runtime_assets_contract.sh +++ b/tests/test_package_runtime_assets_contract.sh @@ -7,7 +7,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -python3 - "$ROOT" <<'PY' +python3 - "$ROOT" "$BASH" <<'PY' from __future__ import annotations import json @@ -26,6 +26,7 @@ import zipfile root = pathlib.Path(sys.argv[1]) +bash_executable = pathlib.Path(sys.argv[2]) failures: list[str] = [] @@ -382,7 +383,7 @@ def package_fixture( archive_name = f"codebase-memory-mcp{'-ui' if variant == 'ui' else ''}-{host_goos}-fixture{archive_suffix}" packaged = subprocess.run( [ - "bash", + str(bash_executable), str(root / "scripts/package-release.sh"), host_goos, "fixture", diff --git a/tests/test_release_archive_extractor_contract.sh b/tests/test_release_archive_extractor_contract.sh index 60ce4ed27..ddb184d7b 100644 --- a/tests/test_release_archive_extractor_contract.sh +++ b/tests/test_release_archive_extractor_contract.sh @@ -6,7 +6,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" -python3 - "$ROOT" <<'PY' +python3 - "$ROOT" "$BASH" <<'PY' from __future__ import annotations import csv @@ -25,6 +25,7 @@ from typing import Dict, List, Optional, Tuple root = pathlib.Path(sys.argv[1]) +bash_executable = pathlib.Path(sys.argv[2]) extractor = root / "scripts/ci/extract-release-archives.sh" unix_targets = ( "linux-amd64", @@ -307,7 +308,13 @@ def invoke( args: Optional[List[str]] = None, ) -> subprocess.CompletedProcess: return subprocess.run( - ["bash", str(extractor), str(archive_dir), str(output_dir), *(args or count_args)], + [ + str(bash_executable), + str(extractor), + str(archive_dir), + str(output_dir), + *(args or count_args), + ], cwd=root, text=True, stdout=subprocess.PIPE, diff --git a/tests/test_venue_parity_contract.sh b/tests/test_venue_parity_contract.sh index 48c4ea2ef..d137c01a5 100755 --- a/tests/test_venue_parity_contract.sh +++ b/tests/test_venue_parity_contract.sh @@ -318,6 +318,31 @@ for local, pattern, why in LOCAL_REQUIRED: if path.exists() and not re.search(pattern, path.read_text()): failures.append(f"{local}: missing required `{pattern}` — {why}") +# Native Windows process lookup searches system locations before PATH. A bare +# `bash` argv launched by embedded Python can therefore select the WSL alias +# instead of the MSYS2 Bash that is already running this contract. Every +# shell-hosted Python block must receive the current `$BASH` as an explicit +# argument and launch that resolved path. Enforce this textually on every +# maintained shell surface so the rule is deterministic on non-Windows hosts +# too, rather than relying on a runner to happen to have the WSL alias. +BARE_BASH_ARGV = re.compile(r"""[\[(]\s*["']bash["']\s*,""") +shell_surfaces = list(root.glob("*.sh")) +for directory in ("tests", "scripts", "test-infrastructure", "pkg"): + base = root / directory + if not base.exists(): + continue + shell_surfaces.extend(base.rglob("*.sh")) +for path in sorted(shell_surfaces): + text = path.read_text(encoding="utf-8") + for match in BARE_BASH_ARGV.finditer(text): + number = text.count("\n", 0, match.start()) + 1 + relative = path.relative_to(root).as_posix() + failures.append( + f"{relative}:{number}: embedded Python must launch the current " + "$BASH by explicit argv path; bare `bash` is WSL-alias-sensitive " + "under native Windows process lookup" + ) + if failures: print("VENUE PARITY CONTRACT VIOLATED — one harness, every venue:") for failure in failures: