From 258fa20565cd697968893663ff67cbc4a3d9ea97 Mon Sep 17 00:00:00 2001 From: Zain Dana Harper <17142659+HarperZ9@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:58:58 -0700 Subject: [PATCH 1/2] Prepare Plexus 0.2.1 with verified GitHub install recipes --- CHANGELOG.md | 16 ++- README.md | 110 +++++++++++++++-- pyproject.toml | 4 +- src/plexus/__init__.py | 11 +- tests/test_release_install_recipes.py | 169 ++++++++++++++++++++++++++ tests/test_release_metadata.py | 47 +++++++ 6 files changed, 340 insertions(+), 17 deletions(-) create mode 100644 tests/test_release_install_recipes.py create mode 100644 tests/test_release_metadata.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bd108f5..4d0cfb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,17 @@ ## Unreleased +No changes yet after the 0.2.1 release-prep branch. + +## 0.2.1 + Honesty repairs to the wiring surface (the credo: color/verdict vocabulary only where a real check ran; no receipt no accept; the honest null is first-class). -- Edges are DECLARED, not probed: plexus never imports, resolves, or runs the - tools, so each edge is tagged `evidence: "declared"` and the "grounded, not - asserted" / "traces back to real code" / "genuinely compose" wording is gone. +- Edges are DECLARED, not probed: declarative discovery never imports, + resolves, or runs the cited tools, so each edge is tagged + `evidence: "declared"` and the "grounded, not asserted" / "traces back to + real code" / "genuinely compose" wording is gone. - Duplicate organ ids are NAMED (`discover().collisions`, `validate` reports `duplicate_organs` and exits 1) instead of collapsing last-writer-wins. - `discover` stamps a re-runnable `receipt`: plexus version, UTC timestamp, and @@ -24,6 +29,11 @@ where a real check ran; no receipt no accept; the honest null is first-class). `mneme.crucible-export/2`, still consumable as `crucible.thesis/1`, and Mneme also declares `mneme.local-origin-recheck/1` as a terminal read-only freshness report capability. +- Add Canon and Relay to the declared capability catalog from public + origin-main source, with repo-relative public paths and no live external lane probing by Plexus. +- Keep this release on the GitHub-asset track: wheel, sdist, and + `SHA256SUMS.txt` are reviewable release assets, while PyPI publication remains + outside this patch. ## 0.2.0 diff --git a/README.md b/README.md index e92a245..90aae8b 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,101 @@ to consumer into a runnable pipeline. Zero runtime dependencies. MCP tells an agent *that tools exist*. plexus tells it *how their outputs plug into each other's inputs*: the layer above a flat tool list. +GitHub-only release install for 0.2.1 on Bash/macOS/Linux: + +```bash +set -euo pipefail +VERSION=0.2.1 +BASE="https://github.com/HarperZ9/plexus/releases/download/v0.2.1" +WHEEL="plexus_mesh-${VERSION}-py3-none-any.whl" +SDIST="plexus_mesh-${VERSION}.tar.gz" +SUMS="SHA256SUMS.txt" +curl -fL -o "$WHEEL" "${BASE}/${WHEEL}" +curl -fL -o "$SDIST" "${BASE}/${SDIST}" +curl -fL -o "$SUMS" "${BASE}/${SUMS}" +python - "$WHEEL" "$SDIST" "$SUMS" <<'PY' +import hashlib +import re +import sys +from pathlib import Path + +required = list(sys.argv[1:3]) +sums = Path(sys.argv[3]) +expected = {} +for line_number, raw_line in enumerate(sums.read_text(encoding="utf-8").splitlines(), 1): + line = raw_line.strip() + if not line: + continue + parts = line.split(maxsplit=1) + if len(parts) != 2: + raise SystemExit(f"malformed checksum line {line_number}") + digest, name = parts[0].lower(), parts[1].lstrip("*") + if not re.fullmatch(r"[0-9a-f]{64}", digest): + raise SystemExit(f"invalid checksum for {name}") + if Path(name).name != name: + raise SystemExit(f"unexpected checksum path: {name}") + if name not in required: + raise SystemExit(f"unexpected checksum entry: {name}") + if name in expected: + raise SystemExit(f"duplicate checksum entry: {name}") + expected[name] = digest +missing = [name for name in required if name not in expected] +if missing: + raise SystemExit(f"missing checksum entry: {', '.join(missing)}") +for name in required: + got = hashlib.sha256(Path(name).read_bytes()).hexdigest() + if got != expected[name]: + raise SystemExit(f"{name}: expected {expected[name]}, got {got}") +PY +python -m pip install "$WHEEL" ``` -pip install git+https://github.com/HarperZ9/plexus.git + +GitHub-only release install for 0.2.1 on native PowerShell: + +```powershell +& { + $ErrorActionPreference = "Stop" + $Version = "0.2.1" + $Base = "https://github.com/HarperZ9/plexus/releases/download/v0.2.1" + $Wheel = "plexus_mesh-$Version-py3-none-any.whl" + $Sdist = "plexus_mesh-$Version.tar.gz" + $Sums = "SHA256SUMS.txt" + $Files = @($Wheel, $Sdist, $Sums) + foreach ($Name in $Files) { + Invoke-WebRequest -Uri "$Base/$Name" -OutFile $Name + } + $Required = @($Wheel, $Sdist) + $Expected = @{} + $LineNumber = 0 + Get-Content -LiteralPath $Sums | ForEach-Object { + $LineNumber += 1 + $Line = $_.Trim() + if (-not $Line) { return } + $Parts = $Line -split '\s+', 2 + if ($Parts.Count -ne 2) { throw "malformed checksum line $LineNumber" } + $Digest = $Parts[0].ToLowerInvariant() + $Name = $Parts[1].TrimStart("*") + if ($Digest -notmatch '^[0-9a-f]{64}$') { throw "invalid checksum for $Name" } + if ([IO.Path]::GetFileName($Name) -ne $Name) { throw "unexpected checksum path: $Name" } + if ($Required -notcontains $Name) { throw "unexpected checksum entry: $Name" } + if ($Expected.ContainsKey($Name)) { throw "duplicate checksum entry: $Name" } + $Expected[$Name] = $Digest + } + foreach ($Name in $Required) { + if (-not $Expected.ContainsKey($Name)) { throw "missing checksum entry: $Name" } + $Got = (Get-FileHash -Algorithm SHA256 -Path $Name).Hash.ToLowerInvariant() + if ($Got -ne $Expected[$Name]) { + throw "${Name}: expected $($Expected[$Name]), got $Got" + } + } + python -m pip install $Wheel +} ``` +`plexus-mesh` is not published on PyPI in this release track. A source branch or +CI run is not a release; install from the GitHub `v0.2.1` assets only after the +wheel, sdist, and `SHA256SUMS.txt` are attached to that release. + ``` $ plexus discover --builtin $ plexus plan --goal crucible @@ -127,8 +218,7 @@ A manifest is plain JSON. A tool ships one and it joins the mesh. Drop An edge `A -> B` forms when `B` consumes a capability that `A` emits (directly, or via `consumable_as`, the way a producer declares "my output is also consumable as X"). Matching is by capability string, so an edge exists wherever -the tools DECLARE compatible capabilities. plexus does not run the tools, so the -edge is a declared claim, not a probed result. +the tools DECLARE compatible capabilities. Declarative discovery does not run the tools, so the edge is a declared claim, not a probed result. Plexus commits the built-in registry's exported JSON manifests under [`manifests/`](manifests/). Discovery from these Plexus-side files produces the @@ -185,9 +275,9 @@ for r in results:

The six keys plexus discover returns for one wiring edge, one to a row, each with what settled it. Four are computed by discovery: the producing organ, the consuming organ, the capability that matched, and whether both ends are the same organ. One is copied out of a manifest without being read: via, the producer's own pointer at the code behind the port. One is a constant: evidence, always the word declared. The via row is accented, because it is the field that looks like a citation and is the one plexus never follows.

Every edge is tagged `evidence: "declared"` and cites the **module** its producer -names as the source (`file:function`). plexus does not import, resolve, or run -that pointer, so the citation is a self-reported claim to check, not a verified -receipt. The running tool re-checks none of the built-in manifests, so treat +names as the source (`file:function`). Declarative discovery does not import, +resolve, or run that pointer, so the citation is a self-reported claim to check, +not a verified receipt. The running tool re-checks none of the built-in manifests, so treat every edge as declared until you follow the pointer yourself. Mneme's contract was refreshed from public main on 2026-09-14, including `mneme.crucible-export/2` and `mneme.local-origin-recheck/1`. Canon and Relay @@ -226,9 +316,15 @@ mesh to exactly the manifests that produced it. ## Install ``` -pip install git+https://github.com/HarperZ9/plexus.git +python -m pip install plexus_mesh-0.2.1-py3-none-any.whl ``` +Use the GitHub release asset and verify it against `SHA256SUMS.txt` first. This +repository does not claim a PyPI publication for `plexus-mesh` in the 0.2.1 +track. The installed package covers declared and synthetic mesh workflows; it +does not prove that real external lanes are live or that `probe_lane()` has been +run against owned services. + ## Library ```python diff --git a/pyproject.toml b/pyproject.toml index 31aae1c..f972c14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "plexus-mesh" -version = "0.2.0" -description = "Capability discovery + auto-wiring for agent toolchains: point it at your tools and it discovers what each one emits and consumes, then wires producer to consumer into an executable pipeline, every edge tagged declared and citing the module its producer names. plexus does not probe or run the tools. Zero runtime dependencies." +version = "0.2.1" +description = "Declarative capability discovery + auto-wiring for agent toolchains: point it at tool manifests and it wires producer to consumer into an executable pipeline, every discovery edge tagged declared and citing the module its producer names. Discovery does not import or run cited tools; optional explicit probe helpers can launch owned MCP servers. Zero runtime dependencies." readme = "README.md" requires-python = ">=3.11" license = "LicenseRef-FSL-1.1-MIT" diff --git a/src/plexus/__init__.py b/src/plexus/__init__.py index a21fd59..627000d 100644 --- a/src/plexus/__init__.py +++ b/src/plexus/__init__.py @@ -1,9 +1,10 @@ """plexus: capability discovery + auto-wiring for agent toolchains. -Point it at a set of tools that ship interop manifests and it discovers what each -one emits and consumes, then wires producer to consumer into a pipeline, every -edge tagged `declared` and carrying the module its producer names. plexus does -not import, probe, or run those tools. Zero runtime dependencies. +Point it at a set of tools that ship interop manifests and declarative discovery +wires producer to consumer into a pipeline, every edge tagged `declared` and +carrying the module its producer names. Discovery does not import or run cited +tools. Optional probe helpers in `plexus.registry` launch owned MCP servers only +when explicitly called. Zero runtime dependencies. """ from .graph import to_dot, to_mermaid from .manifest import Manifest, Port, validate @@ -13,7 +14,7 @@ from .registry import builtin_manifests, load_dir from .run import pipeline_script -__version__ = "0.2.0" +__version__ = "0.2.1" __all__ = [ "Manifest", "Port", "validate", diff --git a/tests/test_release_install_recipes.py b/tests/test_release_install_recipes.py new file mode 100644 index 0000000..4c1dc2e --- /dev/null +++ b/tests/test_release_install_recipes.py @@ -0,0 +1,169 @@ +import hashlib +import os +import re +import shlex +import shutil +import stat +import subprocess +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +VERSION = "0.2.1" +WHEEL = f"plexus_mesh-{VERSION}-py3-none-any.whl" +SDIST = f"plexus_mesh-{VERSION}.tar.gz" + + +def _readme_block(language: str) -> str: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + match = re.search(rf"```{language}\n(.*?)\n```", readme, re.DOTALL) + assert match, f"{language} recipe block missing" + return match.group(1).replace("\r\n", "\n").replace("\r", "\n") + + +def _digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _write_fixture(root: Path, sums_text: str) -> None: + root.mkdir() + (root / WHEEL).write_bytes(b"wheel-bytes") + (root / SDIST).write_bytes(b"sdist-bytes") + (root / "SHA256SUMS.txt").write_text(sums_text, encoding="utf-8") + + +def _sums(case: str) -> str: + wheel_hash = _digest(b"wheel-bytes") + sdist_hash = _digest(b"sdist-bytes") + rows = { + "valid": f"{wheel_hash} {WHEEL}\n{sdist_hash} {SDIST}\n", + "empty": "", + "missing_wheel": f"{sdist_hash} {SDIST}\n", + "bad_hash": f"{'0' * 64} {WHEEL}\n{sdist_hash} {SDIST}\n", + "duplicate": f"{wheel_hash} {WHEEL}\n{wheel_hash} {WHEEL}\n{sdist_hash} {SDIST}\n", + "extra": f"{wheel_hash} {WHEEL}\n{sdist_hash} {SDIST}\n{sdist_hash} extra.whl\n", + "path": f"{wheel_hash} nested/{WHEEL}\n{sdist_hash} {SDIST}\n", + } + return rows[case] + + +def _wsl_path(path: Path) -> str: + resolved = path.resolve().as_posix() + if re.match(r"^[A-Za-z]:/", resolved): + return f"/mnt/{resolved[0].lower()}{resolved[2:]}" + return resolved + + +def _run_bash_recipe(tmp_path: Path, case: str) -> subprocess.CompletedProcess[str]: + if shutil.which("bash") is None: + pytest.skip("bash is not available") + fixture = tmp_path / "fixture" + run_dir = tmp_path / "run" + fake_bin = tmp_path / "fake-bin" + marker = tmp_path / "install-called" + _write_fixture(fixture, _sums(case)) + run_dir.mkdir() + fake_bin.mkdir() + (fake_bin / "curl").write_text( + """#!/usr/bin/env bash +set -euo pipefail +out="" +while [ "$#" -gt 0 ]; do + case "$1" in + -o) out="$2"; shift 2 ;; + *) shift ;; + esac +done +[ -n "$out" ] || { echo "curl mock missing -o output" >&2; exit 1; } +[ -f "$FIXTURE_DIR/$out" ] || { echo "curl mock missing fixture: $out" >&2; exit 1; } +cp "$FIXTURE_DIR/$out" "$out" +""", + encoding="utf-8", + newline="\n", + ) + (fake_bin / "python").write_text( + """#!/usr/bin/env bash +set -euo pipefail +if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ]; then + printf '%s\n' "$*" >> "$INSTALL_MARKER" + exit 0 +fi +exec python3 "$@" +""", + encoding="utf-8", + newline="\n", + ) + for tool in ("curl", "python"): + (fake_bin / tool).chmod((fake_bin / tool).stat().st_mode | stat.S_IXUSR) + script_path = tmp_path / "recipe.sh" + script_path.write_text( + "\n".join( + [ + f'export PATH="{_wsl_path(fake_bin)}:$PATH"', + f"export FIXTURE_DIR={shlex.quote(_wsl_path(fixture))}", + f"export INSTALL_MARKER={shlex.quote(_wsl_path(marker))}", + _readme_block("bash"), + "", + ] + ), + encoding="utf-8", + newline="\n", + ) + return subprocess.run( + ["bash", _wsl_path(script_path)], + cwd=run_dir, + env=os.environ, + text=True, + capture_output=True, + ) + + +def _run_powershell_recipe(tmp_path: Path, case: str) -> subprocess.CompletedProcess[str]: + shell = shutil.which("pwsh") or shutil.which("powershell") + if shell is None: + pytest.skip("PowerShell is not available") + fixture = tmp_path / "fixture" + run_dir = tmp_path / "run" + marker = tmp_path / "install-called" + _write_fixture(fixture, _sums(case)) + run_dir.mkdir() + harness = f""" +function Invoke-WebRequest {{ + param([string]$Uri, [string]$OutFile) + Copy-Item -LiteralPath (Join-Path $env:FIXTURE_DIR $OutFile) -Destination $OutFile -Force +}} +function python {{ + if ($args.Count -ge 4 -and $args[0] -eq "-m" -and $args[1] -eq "pip" -and $args[2] -eq "install") {{ + Add-Content -LiteralPath $env:INSTALL_MARKER -Value ($args -join " ") + return + }} + throw "unexpected python invocation: $($args -join ' ')" +}} +{_readme_block("powershell")} +""" + return subprocess.run( + [shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", harness], + cwd=run_dir, + env={**os.environ, "FIXTURE_DIR": str(fixture), "INSTALL_MARKER": str(marker)}, + text=True, + capture_output=True, + ) + + +@pytest.mark.parametrize("runner", [_run_bash_recipe, _run_powershell_recipe]) +def test_release_recipe_valid_checksums_call_mocked_installer(tmp_path: Path, runner): + completed = runner(tmp_path, "valid") + assert completed.returncode == 0, completed.stderr + completed.stdout + assert (tmp_path / "install-called").read_text(encoding="utf-8").count(WHEEL) == 1 + + +@pytest.mark.parametrize("case", ["empty", "missing_wheel", "bad_hash", "duplicate", "extra", "path"]) +@pytest.mark.parametrize("runner", [_run_bash_recipe, _run_powershell_recipe]) +def test_release_recipe_invalid_checksums_do_not_call_mocked_installer( + tmp_path: Path, case: str, runner +): + completed = runner(tmp_path, case) + assert completed.returncode != 0, completed.stdout + assert not (tmp_path / "install-called").exists() diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py new file mode 100644 index 0000000..ee06d22 --- /dev/null +++ b/tests/test_release_metadata.py @@ -0,0 +1,47 @@ +import re +import tomllib +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +VERSION = "0.2.1" + + +def test_release_identity_is_aligned(): + pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + init_text = (ROOT / "src" / "plexus" / "__init__.py").read_text(encoding="utf-8") + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + + assert pyproject["project"]["version"] == VERSION + assert re.search(rf'__version__ = "{VERSION}"', init_text) + assert f"## {VERSION}" in changelog + + +def test_install_docs_are_github_only_and_hash_verified(): + readme = (ROOT / "README.md").read_text(encoding="utf-8") + + assert "Bash/macOS/Linux" in readme + assert "native PowerShell" in readme + assert "https://github.com/HarperZ9/plexus/releases/download/v0.2.1" in readme + assert "plexus_mesh-0.2.1-py3-none-any.whl" in readme + assert "SHA256SUMS.txt" in readme + assert "Get-FileHash -Algorithm SHA256" in readme + assert "Invoke-WebRequest" in readme + assert "not published on PyPI" in readme + assert "pip install git+https://github.com/HarperZ9/plexus.git" not in readme + + +def test_release_docs_preserve_declared_not_probed_boundary(): + readme = (ROOT / "README.md").read_text(encoding="utf-8").lower() + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8").lower() + pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + init_text = (ROOT / "src" / "plexus" / "__init__.py").read_text(encoding="utf-8").lower() + + assert "declared, not probed" in readme + assert "declarative discovery does not run the tools" in readme + assert "does not prove that real external lanes are live" in readme + assert "no live external lane probing" in changelog + assert "optional explicit probe helpers can launch owned mcp servers" in pyproject["project"]["description"].lower() + assert "optional probe helpers" in init_text + assert "does not import, probe, or run those tools" not in init_text + assert "plexus does not probe or run the tools" not in pyproject["project"]["description"].lower() From 012afa43ca98ab30084f2a4ab77c486382167a0d Mon Sep 17 00:00:00 2001 From: Zain Dana Harper <17142659+HarperZ9@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:19:58 -0700 Subject: [PATCH 2/2] Use working Git Bash for Windows release recipe checks --- tests/test_release_install_recipes.py | 115 ++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 8 deletions(-) diff --git a/tests/test_release_install_recipes.py b/tests/test_release_install_recipes.py index 4c1dc2e..1878962 100644 --- a/tests/test_release_install_recipes.py +++ b/tests/test_release_install_recipes.py @@ -5,6 +5,7 @@ import shutil import stat import subprocess +import sys from pathlib import Path import pytest @@ -14,6 +15,7 @@ VERSION = "0.2.1" WHEEL = f"plexus_mesh-{VERSION}-py3-none-any.whl" SDIST = f"plexus_mesh-{VERSION}.tar.gz" +SUMS = "SHA256SUMS.txt" def _readme_block(language: str) -> str: @@ -56,13 +58,76 @@ def _wsl_path(path: Path) -> str: return resolved +def _git_bash_path(path: Path) -> str: + resolved = path.resolve().as_posix() + if re.match(r"^[A-Za-z]:/", resolved): + return f"/{resolved[0].lower()}{resolved[2:]}" + return resolved + + +def _bash_candidates() -> list[str]: + if os.name != "nt": + found = shutil.which("bash") + return [found] if found else [] + prefixes = [ + os.environ.get("ProgramFiles", r"C:\Program Files"), + os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"), + ] + candidates = [] + for prefix in prefixes: + candidates.extend([ + str(Path(prefix) / "Git" / "bin" / "bash.exe"), + str(Path(prefix) / "Git" / "usr" / "bin" / "bash.exe"), + ]) + found = shutil.which("bash") + if found: + candidates.append(found) + return list(dict.fromkeys(candidates)) + + +def _probe_bash(command: str, *, require_msys: bool) -> bool: + if not command or not Path(command).exists(): + return False + try: + completed = subprocess.run( + [command, "--noprofile", "--norc", "-c", "printf '%s' \"$BASH_VERSION\"; printf ' '; uname -s 2>/dev/null || true"], + text=True, + capture_output=True, + timeout=8, + ) + except (OSError, subprocess.TimeoutExpired): + return False + if completed.returncode != 0 or not completed.stdout.strip(): + return False + if require_msys and not any(tag in completed.stdout for tag in ("MINGW", "MSYS", "CYGWIN")): + return False + return True + + +def _select_bash(candidates: list[str] | None = None, *, require_msys: bool | None = None) -> str | None: + require = os.name == "nt" if require_msys is None else require_msys + for command in candidates or _bash_candidates(): + if _probe_bash(command, require_msys=require): + return command + return None + + +def _bash_path(path: Path, *, git_bash: bool) -> str: + if os.name == "nt": + return _git_bash_path(path) if git_bash else _wsl_path(path) + return path.resolve().as_posix() + + def _run_bash_recipe(tmp_path: Path, case: str) -> subprocess.CompletedProcess[str]: - if shutil.which("bash") is None: - pytest.skip("bash is not available") + bash = _select_bash() + if bash is None: + pytest.skip("working Bash is not available") + git_bash = os.name == "nt" fixture = tmp_path / "fixture" run_dir = tmp_path / "run" fake_bin = tmp_path / "fake-bin" marker = tmp_path / "install-called" + download_marker = tmp_path / "download-called" _write_fixture(fixture, _sums(case)) run_dir.mkdir() fake_bin.mkdir() @@ -78,6 +143,7 @@ def _run_bash_recipe(tmp_path: Path, case: str) -> subprocess.CompletedProcess[s done [ -n "$out" ] || { echo "curl mock missing -o output" >&2; exit 1; } [ -f "$FIXTURE_DIR/$out" ] || { echo "curl mock missing fixture: $out" >&2; exit 1; } +printf '%s\n' "$out" >> "$DOWNLOAD_MARKER" cp "$FIXTURE_DIR/$out" "$out" """, encoding="utf-8", @@ -90,7 +156,7 @@ def _run_bash_recipe(tmp_path: Path, case: str) -> subprocess.CompletedProcess[s printf '%s\n' "$*" >> "$INSTALL_MARKER" exit 0 fi -exec python3 "$@" +exec "$REAL_PYTHON" "$@" """, encoding="utf-8", newline="\n", @@ -101,9 +167,11 @@ def _run_bash_recipe(tmp_path: Path, case: str) -> subprocess.CompletedProcess[s script_path.write_text( "\n".join( [ - f'export PATH="{_wsl_path(fake_bin)}:$PATH"', - f"export FIXTURE_DIR={shlex.quote(_wsl_path(fixture))}", - f"export INSTALL_MARKER={shlex.quote(_wsl_path(marker))}", + f'export PATH="{_bash_path(fake_bin, git_bash=git_bash)}:$PATH"', + f"export FIXTURE_DIR={shlex.quote(_bash_path(fixture, git_bash=git_bash))}", + f"export INSTALL_MARKER={shlex.quote(_bash_path(marker, git_bash=git_bash))}", + f"export DOWNLOAD_MARKER={shlex.quote(_bash_path(download_marker, git_bash=git_bash))}", + f"export REAL_PYTHON={shlex.quote(_bash_path(Path(sys.executable), git_bash=git_bash))}", _readme_block("bash"), "", ] @@ -112,7 +180,7 @@ def _run_bash_recipe(tmp_path: Path, case: str) -> subprocess.CompletedProcess[s newline="\n", ) return subprocess.run( - ["bash", _wsl_path(script_path)], + [bash, _bash_path(script_path, git_bash=git_bash)], cwd=run_dir, env=os.environ, text=True, @@ -127,12 +195,14 @@ def _run_powershell_recipe(tmp_path: Path, case: str) -> subprocess.CompletedPro fixture = tmp_path / "fixture" run_dir = tmp_path / "run" marker = tmp_path / "install-called" + download_marker = tmp_path / "download-called" _write_fixture(fixture, _sums(case)) run_dir.mkdir() harness = f""" function Invoke-WebRequest {{ param([string]$Uri, [string]$OutFile) Copy-Item -LiteralPath (Join-Path $env:FIXTURE_DIR $OutFile) -Destination $OutFile -Force + Add-Content -LiteralPath $env:DOWNLOAD_MARKER -Value $OutFile }} function python {{ if ($args.Count -ge 4 -and $args[0] -eq "-m" -and $args[1] -eq "pip" -and $args[2] -eq "install") {{ @@ -146,16 +216,44 @@ def _run_powershell_recipe(tmp_path: Path, case: str) -> subprocess.CompletedPro return subprocess.run( [shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", harness], cwd=run_dir, - env={**os.environ, "FIXTURE_DIR": str(fixture), "INSTALL_MARKER": str(marker)}, + env={ + **os.environ, + "FIXTURE_DIR": str(fixture), + "INSTALL_MARKER": str(marker), + "DOWNLOAD_MARKER": str(download_marker), + }, text=True, capture_output=True, ) +def _assert_downloads_ran(tmp_path: Path) -> None: + downloads = (tmp_path / "download-called").read_text(encoding="utf-8").splitlines() + assert downloads == [WHEEL, SDIST, SUMS] + + +def test_windows_bash_selection_rejects_wsl_alias_before_git_bash(monkeypatch): + def fake_exists(self): + return True + + def fake_run(args, **kwargs): + if args[0] == "wsl-bash": + return subprocess.CompletedProcess(args, 1, "Windows Subsystem for Linux has no installed distributions.", "") + if args[0] == "git-bash": + return subprocess.CompletedProcess(args, 0, "5.2.37 MINGW64_NT-10.0", "") + raise AssertionError(args) + + monkeypatch.setattr(Path, "exists", fake_exists) + monkeypatch.setattr(subprocess, "run", fake_run) + + assert _select_bash(["wsl-bash", "git-bash"], require_msys=True) == "git-bash" + + @pytest.mark.parametrize("runner", [_run_bash_recipe, _run_powershell_recipe]) def test_release_recipe_valid_checksums_call_mocked_installer(tmp_path: Path, runner): completed = runner(tmp_path, "valid") assert completed.returncode == 0, completed.stderr + completed.stdout + _assert_downloads_ran(tmp_path) assert (tmp_path / "install-called").read_text(encoding="utf-8").count(WHEEL) == 1 @@ -166,4 +264,5 @@ def test_release_recipe_invalid_checksums_do_not_call_mocked_installer( ): completed = runner(tmp_path, case) assert completed.returncode != 0, completed.stdout + _assert_downloads_ran(tmp_path) assert not (tmp_path / "install-called").exists()