diff --git a/cron/scheduler.py b/cron/scheduler.py index 90683b6cc1c6..c5f2b9eb0b1d 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -27,7 +27,7 @@ import msvcrt except ImportError: msvcrt = None -from pathlib import Path +from pathlib import Path, PurePath from typing import List, Optional # Add parent directory to path for imports BEFORE repo-level imports. @@ -693,6 +693,13 @@ def _get_script_timeout() -> int: return _DEFAULT_SCRIPT_TIMEOUT +def _format_bash_script_path(path: PurePath) -> str: + """Return a bash-safe script path for the current platform.""" + if os.name == "nt": + return path.as_posix() + return str(path) + + def _run_job_script(script_path: str) -> tuple[bool, str]: """Execute a cron job's data-collection script and capture its output. @@ -769,7 +776,7 @@ def _run_job_script(script_path: str) -> tuple[bool, str]: "On Windows, install Git for Windows (which ships Git Bash) " "or rewrite the script as Python (.py)." ) - argv = [_bash, str(path)] + argv = [_bash, _format_bash_script_path(path)] else: argv = [sys.executable, str(path)] diff --git a/tests/cron/test_scheduler.py b/tests/cron/test_scheduler.py index e0cb1cc155ed..10b3f865b347 100644 --- a/tests/cron/test_scheduler.py +++ b/tests/cron/test_scheduler.py @@ -3,11 +3,12 @@ import json import logging import os +from pathlib import PurePosixPath, PureWindowsPath from unittest.mock import AsyncMock, patch, MagicMock import pytest -from cron.scheduler import _resolve_origin, _resolve_delivery_target, _deliver_result, _send_media_via_adapter, run_job, SILENT_MARKER, _build_job_prompt +from cron.scheduler import _resolve_origin, _resolve_delivery_target, _deliver_result, _send_media_via_adapter, run_job, SILENT_MARKER, _build_job_prompt, _format_bash_script_path from tools.env_passthrough import clear_env_passthrough from tools.credential_files import clear_credential_files @@ -70,6 +71,24 @@ def test_non_dict_origin_returns_none_instead_of_crashing(self, non_dict_origin) assert _resolve_origin(job) is None +class TestFormatBashScriptPath: + def test_uses_forward_slashes_on_windows(self): + with patch("cron.scheduler.os.name", "nt"): + formatted = _format_bash_script_path( + PureWindowsPath(r"C:\Users\denis\.hermes\scripts\hermes-backup.sh") + ) + + assert formatted == "C:/Users/denis/.hermes/scripts/hermes-backup.sh" + + def test_keeps_posix_paths_unchanged(self): + path = PurePosixPath("/home/denis/.hermes/scripts/hermes-backup.sh") + + with patch("cron.scheduler.os.name", "posix"): + formatted = _format_bash_script_path(path) + + assert formatted == str(path) + + class TestResolveDeliveryTarget: def test_origin_delivery_preserves_thread_id(self): job = {