test(reset): select a working Bash instead of an unusable WSL launcher (#671)

* test(reset): probe Bash variable expansion on Windows

Use a bounded variable-expansion probe so a launcher that starts successfully but consumes $x is rejected. Rebase over #676 and keep its upstream-checkout guard alongside Bash discovery.

Thanks to MadsLorentzen for diagnosing the launcher expansion failure and verifying the stronger probe.

* changelog: place the #671 entry under [Unreleased]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-20 17:26:53 +02:00
1 parent 64a6c90ec3
commit 215dfb5af9
5 files changed
+118 -3

No files matched your search

+8
View File
@@ -263,6 +263,14 @@ per-file diff commands.
`MadsLorentzen/ai-job-search/1.0 (freehire-search)`. The host-check test now lists `github.com` and
`gist.github.com` postings among the hosts that must stay unverified.
- **The `/reset` replay tests find a Bash that actually runs the script on Windows** (#671,
`tests/_bash.py`, `tests/test_bash.py`, `tests/test_reset_command.py`, `tests/test_reset_documents.py`) -
the tests replayed the documented `/reset` commands with whichever `bash` came first on PATH. On
Windows that is often the WSL launcher, which expands variables before WSL's bash sees them, so the
replayed script deleted nothing and two tests failed. Each candidate is now probed with
`x=ok; test "$x" = ok`, Git for Windows' own Bash is tried when the launcher fails the probe, and the
replay checks skip only when no usable shell is found.
### Security
- **CI no longer leaves the job token in `.git/config` or expands a PR-controlled name into a
+31
View File
@@ -0,0 +1,31 @@
"""Find a usable Bash for tests that replay the documented reset commands."""
import os
from pathlib import Path
import shutil
import subprocess
WINDOWS = os.name == "nt"
def find_bash():
candidates = [shutil.which("bash")]
if WINDOWS:
# A Windows PATH may find the WSL launcher even without a usable WSL
# installation. Git for Windows also ships Bash beside its cmd/ entry
# points, but does not normally add that bin/ directory to PATH.
git = shutil.which("git")
if git:
candidates.append(str(Path(git).resolve().parent.parent / "bin" / "bash.exe"))
for candidate in dict.fromkeys(candidates):
if not candidate:
continue
try:
result = subprocess.run(
[candidate, "-c", 'x=ok; test "$x" = ok'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5,
)
except (OSError, subprocess.SubprocessError):
continue
if result.returncode == 0:
return candidate
return None
+70
View File
@@ -0,0 +1,70 @@
"""A Bash launcher on PATH is not necessarily a usable shell (Windows WSL)."""
from pathlib import Path
import subprocess
import unittest
from unittest import mock
try:
from tests import _bash
except ImportError:
import _bash
class BashDiscoveryTests(unittest.TestCase):
def test_working_path_shell_is_used(self):
with mock.patch.object(_bash, "WINDOWS", False), \
mock.patch.object(_bash.shutil, "which", return_value="/bin/bash"), \
mock.patch.object(_bash.subprocess, "run", return_value=mock.Mock(returncode=0)) as run:
self.assertEqual(_bash.find_bash(), "/bin/bash")
self.assertEqual(run.call_args.args[0], ["/bin/bash", "-c", 'x=ok; test "$x" = ok'])
def test_launcher_that_expands_variables_falls_back_to_git_bash(self):
git = Path(__file__).resolve().parent / "Git" / "cmd" / "git.exe"
fallback = str(git.parent.parent / "bin" / "bash.exe")
def probe(args, **kwargs):
script = args[2]
if args[0] == "wsl-bash":
# The launcher can exit successfully, but expands $x before
# Bash assigns it, so the reset command cannot work.
script = script.replace("$x", "")
return subprocess.CompletedProcess(args, 0 if script in (
"exit 0", 'x=ok; test "$x" = ok',
) else 1)
with mock.patch.object(_bash, "WINDOWS", True), \
mock.patch.object(_bash.shutil, "which", side_effect=["wsl-bash", str(git)]), \
mock.patch.object(_bash.subprocess, "run", side_effect=probe):
self.assertEqual(_bash.find_bash(), fallback)
def test_missing_shell_needs_no_probe(self):
with mock.patch.object(_bash, "WINDOWS", False), \
mock.patch.object(_bash.shutil, "which", return_value=None), \
mock.patch.object(_bash.subprocess, "run") as run:
self.assertIsNone(_bash.find_bash())
run.assert_not_called()
def test_unusable_path_shell_falls_back_to_git_bash_on_windows(self):
git = Path(__file__).resolve().parent / "Git" / "cmd" / "git.exe"
fallback = str(git.parent.parent / "bin" / "bash.exe")
with mock.patch.object(_bash, "WINDOWS", True), \
mock.patch.object(_bash.shutil, "which", side_effect=["wsl-bash", str(git)]), \
mock.patch.object(_bash.subprocess, "run", side_effect=[
mock.Mock(returncode=1), mock.Mock(returncode=0),
]) as run:
self.assertEqual(_bash.find_bash(), fallback)
self.assertEqual([call.args[0][0] for call in run.call_args_list], ["wsl-bash", fallback])
def test_failed_or_hanging_shell_is_unavailable(self):
for error in (OSError("cannot start"), subprocess.TimeoutExpired("bash", 5)):
with self.subTest(error=type(error).__name__), \
mock.patch.object(_bash, "WINDOWS", False), \
mock.patch.object(_bash.shutil, "which", return_value="bash"), \
mock.patch.object(_bash.subprocess, "run", side_effect=error):
self.assertIsNone(_bash.find_bash())
def test_nonzero_exit_without_a_fallback_is_unavailable(self):
with mock.patch.object(_bash, "WINDOWS", False), \
mock.patch.object(_bash.shutil, "which", return_value="bash"), \
mock.patch.object(_bash.subprocess, "run", return_value=mock.Mock(returncode=1)):
self.assertIsNone(_bash.find_bash())
+3 -2
View File
@@ -28,21 +28,22 @@ tree, the profile files from /setup Step 3's own headings - so a new drop
folder or a new /setup target fails this test until /reset covers it.
"""
import re
import shutil
import subprocess
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
try:
from tests._bash import find_bash
from tests._upstream import is_upstream_checkout
except ImportError: # `python -m unittest discover -s tests` imports test modules top-level
from _bash import find_bash
from _upstream import is_upstream_checkout
REPO = Path(__file__).resolve().parent.parent
RESET = REPO / ".claude" / "commands" / "reset.md"
SETUP = REPO / ".claude" / "commands" / "setup.md"
BASH = shutil.which("bash")
BASH = find_bash()
def tracked_document_subfolders():
+6 -1
View File
@@ -8,9 +8,14 @@ import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
try:
from tests._bash import find_bash
except ImportError: # unittest discover -s tests imports modules top-level
from _bash import find_bash
COMMAND = Path(__file__).resolve().parent.parent / ".claude/commands/reset.md"
FOLDERS = ("cv", "linkedin", "diplomas", "references", "projects", "postings", "applications")
BASH = shutil.which("bash")
BASH = find_bash()
def symlinks_available():