Compare commits
1 Commits
claude/iss
...
whip/372-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e17e421c76 |
@@ -26,7 +26,7 @@ from cron.jobs import (
|
||||
trigger_job,
|
||||
JOBS_FILE,
|
||||
)
|
||||
from cron.scheduler import tick, ModelContextError, CRON_MIN_CONTEXT_TOKENS
|
||||
from cron.scheduler import tick
|
||||
|
||||
__all__ = [
|
||||
"create_job",
|
||||
@@ -39,6 +39,4 @@ __all__ = [
|
||||
"trigger_job",
|
||||
"tick",
|
||||
"JOBS_FILE",
|
||||
"ModelContextError",
|
||||
"CRON_MIN_CONTEXT_TOKENS",
|
||||
]
|
||||
|
||||
@@ -376,6 +376,7 @@ def create_job(
|
||||
provider: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
script: Optional[str] = None,
|
||||
requires_local_infra: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new cron job.
|
||||
@@ -386,7 +387,7 @@ def create_job(
|
||||
name: Optional friendly name
|
||||
repeat: How many times to run (None = forever, 1 = once)
|
||||
deliver: Where to deliver output ("origin", "local", "telegram", etc.)
|
||||
origin: Source info where job was created (for "origin" delivery)
|
||||
origin: Source info where job was created ("origin" delivery)
|
||||
skill: Optional legacy single skill name to load before running the prompt
|
||||
skills: Optional ordered list of skills to load before running the prompt
|
||||
model: Optional per-job model override
|
||||
@@ -395,6 +396,8 @@ def create_job(
|
||||
script: Optional path to a Python script whose stdout is injected into the
|
||||
prompt each run. The script runs before the agent turn, and its output
|
||||
is prepended as context. Useful for data collection / change detection.
|
||||
requires_local_infra: If True, job requires local infrastructure (SSH keys,
|
||||
localhost access, etc.). Terminal toolset is disabled on cloud providers.
|
||||
|
||||
Returns:
|
||||
The created job dict
|
||||
@@ -455,6 +458,8 @@ def create_job(
|
||||
# Delivery configuration
|
||||
"deliver": deliver,
|
||||
"origin": origin, # Tracks where job was created for "origin" delivery
|
||||
# Infrastructure requirements
|
||||
"requires_local_infra": requires_local_infra,
|
||||
}
|
||||
|
||||
jobs = load_jobs()
|
||||
|
||||
126
tests/test_cron_requires_local_infra.py
Normal file
126
tests/test_cron_requires_local_infra.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Tests for cron job requires_local_infra field.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from cron.jobs import create_job, get_job, load_jobs, save_jobs
|
||||
|
||||
|
||||
class TestRequiresLocalInfra:
|
||||
"""Test the requires_local_infra field in cron jobs."""
|
||||
|
||||
@pytest.fixture
|
||||
def cron_env(self, tmp_path, monkeypatch):
|
||||
"""Set up a temporary cron environment."""
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
cron_dir = hermes_home / "cron"
|
||||
cron_dir.mkdir()
|
||||
jobs_file = cron_dir / "jobs.json"
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setattr("cron.jobs.JOBS_FILE", jobs_file)
|
||||
monkeypatch.setattr("cron.jobs.HERMES_DIR", hermes_home)
|
||||
monkeypatch.setattr("cron.jobs.CRON_DIR", cron_dir)
|
||||
|
||||
return {"hermes_home": hermes_home, "jobs_file": jobs_file}
|
||||
|
||||
def test_create_job_default_requires_local_infra_false(self, cron_env):
|
||||
"""By default, requires_local_infra should be False."""
|
||||
job = create_job(
|
||||
prompt="Test job",
|
||||
schedule="every 1h",
|
||||
name="Test",
|
||||
)
|
||||
assert job.get("requires_local_infra") is False
|
||||
|
||||
def test_create_job_requires_local_infra_true(self, cron_env):
|
||||
"""Can create a job with requires_local_infra=True."""
|
||||
job = create_job(
|
||||
prompt="SSH into server and check status",
|
||||
schedule="every 1h",
|
||||
name="SSH Check",
|
||||
requires_local_infra=True,
|
||||
)
|
||||
assert job.get("requires_local_infra") is True
|
||||
|
||||
def test_requires_local_infra_persists(self, cron_env):
|
||||
"""requires_local_infra should persist to jobs.json."""
|
||||
job = create_job(
|
||||
prompt="Check Ollama is responding",
|
||||
schedule="every 30m",
|
||||
name="Ollama Health",
|
||||
requires_local_infra=True,
|
||||
)
|
||||
|
||||
# Reload jobs from disk
|
||||
jobs = load_jobs()
|
||||
loaded_job = next(j for j in jobs if j["id"] == job["id"])
|
||||
assert loaded_job.get("requires_local_infra") is True
|
||||
|
||||
def test_requires_local_infra_false_persists(self, cron_env):
|
||||
"""requires_local_infra=False should also persist."""
|
||||
job = create_job(
|
||||
prompt="Simple cloud check",
|
||||
schedule="every 1h",
|
||||
name="Cloud Check",
|
||||
requires_local_infra=False,
|
||||
)
|
||||
|
||||
jobs = load_jobs()
|
||||
loaded_job = next(j for j in jobs if j["id"] == job["id"])
|
||||
assert loaded_job.get("requires_local_infra") is False
|
||||
|
||||
def test_legacy_jobs_without_requires_local_infra(self, cron_env):
|
||||
"""Legacy jobs without requires_local_infra should default to False."""
|
||||
# Create a job dict without requires_local_infra (simulating legacy job)
|
||||
legacy_job = {
|
||||
"id": "legacy123",
|
||||
"name": "Legacy Job",
|
||||
"prompt": "Old job",
|
||||
"schedule": {"kind": "interval", "minutes": 60},
|
||||
"schedule_display": "every 1h",
|
||||
"enabled": True,
|
||||
"state": "scheduled",
|
||||
}
|
||||
|
||||
# Save directly to jobs.json
|
||||
save_jobs([legacy_job])
|
||||
|
||||
# Load and check
|
||||
jobs = load_jobs()
|
||||
loaded_job = jobs[0]
|
||||
# Should not have requires_local_infra key, but get() returns None
|
||||
assert loaded_job.get("requires_local_infra") is None
|
||||
|
||||
|
||||
class TestCronjobToolRequiresLocalInfra:
|
||||
"""Test the cronjob tool with requires_local_infra parameter."""
|
||||
|
||||
def test_tool_schema_includes_requires_local_infra(self):
|
||||
"""The tool schema should include requires_local_infra parameter."""
|
||||
from tools.cronjob_tools import CRONJOB_SCHEMA
|
||||
|
||||
params = CRONJOB_SCHEMA["parameters"]["properties"]
|
||||
assert "requires_local_infra" in params
|
||||
assert params["requires_local_infra"]["type"] == "boolean"
|
||||
assert "default" in params["requires_local_infra"]
|
||||
|
||||
def test_cronjob_function_accepts_requires_local_infra(self):
|
||||
"""The cronjob function should accept requires_local_infra parameter."""
|
||||
from tools.cronjob_tools import cronjob
|
||||
import inspect
|
||||
|
||||
sig = inspect.signature(cronjob)
|
||||
assert "requires_local_infra" in sig.parameters
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -233,6 +233,7 @@ def cronjob(
|
||||
base_url: Optional[str] = None,
|
||||
reason: Optional[str] = None,
|
||||
script: Optional[str] = None,
|
||||
requires_local_infra: bool = False,
|
||||
task_id: str = None,
|
||||
) -> str:
|
||||
"""Unified cron job management tool."""
|
||||
@@ -270,6 +271,7 @@ def cronjob(
|
||||
provider=_normalize_optional_job_value(provider),
|
||||
base_url=_normalize_optional_job_value(base_url, strip_trailing_slash=True),
|
||||
script=_normalize_optional_job_value(script),
|
||||
requires_local_infra=requires_local_infra,
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
@@ -506,6 +508,11 @@ Important safety rule: cron-run sessions should not recursively schedule more cr
|
||||
"type": "string",
|
||||
"description": "Optional path to a Python script that runs before each cron job execution. Its stdout is injected into the prompt as context. Use for data collection and change detection. Relative paths resolve under ~/.hermes/scripts/. On update, pass empty string to clear."
|
||||
},
|
||||
"requires_local_infra": {
|
||||
"type": "boolean",
|
||||
"description": "If true, job requires local infrastructure (SSH keys, localhost access, etc.). Terminal toolset is disabled on cloud providers. Use for jobs that SSH into servers or check local services like Ollama.",
|
||||
"default": False
|
||||
},
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user