Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cron/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ def _summarize_cron_failure_for_delivery(job: dict, error: str | None) -> str:
text = (error or "unknown error").strip()
lower = text.lower()

# _run_job_script emits this exact envelope for local subprocess
# timeouts. Classify it before broad provider-keyword checks so a script
# path containing "429" or "rate-limit" cannot shadow the real cause.
if re.match(r"^script timed out after \d+(?:\.\d+)?s:", lower):
return (
f"⚠️ Cron '{job_name}' failed: local script timeout. "
"The pre-run script exceeded its execution limit. "
"Full details saved in cron output."
)

# Provider/API failures are the common noisy path. Keep these short.
if "429" in text or "rate limit" in lower or "usage limit" in lower:
reason = "rate limit"
Expand Down
29 changes: 29 additions & 0 deletions tests/cron/test_cron_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,35 @@ def test_script_timeout(self, cron_env, monkeypatch):
assert success is False
assert "timed out" in output.lower()

def test_script_timeout_delivery_summary_stays_local(self, cron_env, monkeypatch):
import subprocess

from cron import scheduler as sched_mod
from cron.scheduler import (
_run_job_script,
_summarize_cron_failure_for_delivery,
)

script = cron_env / "scripts" / "429-rate-limit-report.py"
script.write_text("print('never runs')\n")

def raise_timeout(*args, **kwargs):
raise subprocess.TimeoutExpired(cmd=args[0], timeout=kwargs["timeout"])

monkeypatch.setattr(sched_mod.subprocess, "run", raise_timeout)
success, error = _run_job_script(str(script))

assert success is False
assert error.startswith("Script timed out after ")
summary = _summarize_cron_failure_for_delivery(
{"name": "nightly-report"}, error
)
assert "local script timeout" in summary
assert "provider timeout" not in summary
assert "provider rate limit" not in summary
assert "Fallback chain" not in summary
assert str(script) not in summary

def test_script_json_output(self, cron_env):
"""Scripts can output structured JSON for the LLM to parse."""
from cron.scheduler import _run_job_script
Expand Down
61 changes: 61 additions & 0 deletions tests/cron/test_scheduler_failure_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Regression tests for concise cron failure delivery summaries."""

from cron.scheduler import _summarize_cron_failure_for_delivery


JOB = {"id": "job-1", "name": "nightly-report"}


def summarize(error: str) -> str:
return _summarize_cron_failure_for_delivery(JOB, error)


def test_local_script_timeout_is_not_labelled_as_provider_failure():
result = summarize("Script timed out after 120s: /tmp/foo.py")

assert "local script timeout" in result
assert "provider timeout" not in result
assert "Fallback chain" not in result
assert "/tmp/foo.py" not in result


def test_decimal_local_script_timeout_is_classified_locally():
result = summarize("Script timed out after 1.5s: /tmp/foo.py")

assert "local script timeout" in result
assert "provider timeout" not in result


def test_local_timeout_path_containing_429_does_not_become_rate_limit():
result = summarize("Script timed out after 30s: /tmp/429-report.py")

assert "local script timeout" in result
assert "provider rate limit" not in result


def test_local_timeout_path_containing_rate_limit_stays_local():
result = summarize("Script timed out after 30s: /tmp/rate-limit-report.py")

assert "local script timeout" in result
assert "provider rate limit" not in result


def test_readtimeout_remains_a_provider_timeout():
result = summarize("ReadTimeout: request timed out")

assert "provider timeout" in result
assert "Fallback chain" in result


def test_normal_script_failure_remains_generic_and_sanitized():
result = summarize("Script execution failed: permission denied")

assert result == "⚠️ Cron 'nightly-report' failed: Script execution failed: permission denied"
assert "provider timeout" not in result


def test_incidental_timeout_wording_does_not_match_local_script_envelope():
result = summarize("Script reported timeout statistics")

assert "local script timeout" not in result
assert "provider timeout" in result