From 5b87cca886c98d47b0dcbf00a7363320d935c82e Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 26 Apr 2026 17:16:14 -0400 Subject: [PATCH] fix(xurl): treat PermissionError from PATH lookup as unavailable (#322) is_available() only caught FileNotFoundError and TimeoutExpired. On WSL, a /mnt/c/.../WindowsApps entry on $PATH returns EACCES during exec, and Python raises PermissionError. That escaped is_available() and crashed pipeline.diagnose() before any source ran. Catch OSError instead. It covers FileNotFoundError, PermissionError, and any other spawn-time OS error, so a non-executable xurl on PATH falls through to the next backend instead of aborting the run. --- skills/last30days/scripts/lib/xurl_x.py | 7 ++++--- tests/test_xurl_x.py | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/skills/last30days/scripts/lib/xurl_x.py b/skills/last30days/scripts/lib/xurl_x.py index 994b58c..c2a60ef 100644 --- a/skills/last30days/scripts/lib/xurl_x.py +++ b/skills/last30days/scripts/lib/xurl_x.py @@ -46,9 +46,10 @@ def is_available() -> bool: timeout=10, ) return result.returncode == 0 and '"username"' in result.stdout - except FileNotFoundError: - return False - except subprocess.TimeoutExpired: + except (OSError, subprocess.TimeoutExpired): + # OSError covers FileNotFoundError (no xurl on PATH) and + # PermissionError (a non-executable match on PATH, e.g. WSL's + # /mnt/c/.../WindowsApps shim returning EACCES on exec). return False diff --git a/tests/test_xurl_x.py b/tests/test_xurl_x.py index 14bce72..7f79e09 100644 --- a/tests/test_xurl_x.py +++ b/tests/test_xurl_x.py @@ -44,6 +44,13 @@ class TestIsAvailable(unittest.TestCase): with mock.patch("subprocess.run", side_effect=FileNotFoundError): self.assertFalse(xurl_x.is_available()) + def test_returns_false_on_permission_error(self): + # WSL hits this when a Windows-mounted PATH entry points at an + # exec-blocked shim (e.g. WindowsApps), which raises PermissionError + # before any other PATH candidate is tried. + with mock.patch("subprocess.run", side_effect=PermissionError(13, "Permission denied", "xurl")): + self.assertFalse(xurl_x.is_available()) + def test_returns_false_on_timeout(self): import subprocess with mock.patch("subprocess.run", side_effect=subprocess.TimeoutExpired("xurl", 10)):