feat: add multi-skill cron editing and docs
This commit is contained in:
parent
df5c61b37c
commit
c3ea620796
19 changed files with 968 additions and 407 deletions
|
|
@ -248,3 +248,49 @@ class TestRunJobSkillBacked:
|
|||
assert "blogwatcher" in prompt_arg
|
||||
assert "Follow this skill" in prompt_arg
|
||||
assert "Check the feeds and summarize anything new." in prompt_arg
|
||||
|
||||
def test_run_job_loads_multiple_skills_in_order(self, tmp_path):
|
||||
job = {
|
||||
"id": "multi-skill-job",
|
||||
"name": "multi skill test",
|
||||
"prompt": "Combine the results.",
|
||||
"skills": ["blogwatcher", "find-nearby"],
|
||||
}
|
||||
|
||||
fake_db = MagicMock()
|
||||
|
||||
def _skill_view(name):
|
||||
return json.dumps({"success": True, "content": f"# {name}\nInstructions for {name}."})
|
||||
|
||||
with patch("cron.scheduler._hermes_home", tmp_path), \
|
||||
patch("cron.scheduler._resolve_origin", return_value=None), \
|
||||
patch("dotenv.load_dotenv"), \
|
||||
patch("hermes_state.SessionDB", return_value=fake_db), \
|
||||
patch(
|
||||
"hermes_cli.runtime_provider.resolve_runtime_provider",
|
||||
return_value={
|
||||
"api_key": "***",
|
||||
"base_url": "https://example.invalid/v1",
|
||||
"provider": "openrouter",
|
||||
"api_mode": "chat_completions",
|
||||
},
|
||||
), \
|
||||
patch("tools.skills_tool.skill_view", side_effect=_skill_view) as skill_view_mock, \
|
||||
patch("run_agent.AIAgent") as mock_agent_cls:
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.run_conversation.return_value = {"final_response": "ok"}
|
||||
mock_agent_cls.return_value = mock_agent
|
||||
|
||||
success, output, final_response, error = run_job(job)
|
||||
|
||||
assert success is True
|
||||
assert error is None
|
||||
assert final_response == "ok"
|
||||
assert skill_view_mock.call_count == 2
|
||||
assert [call.args[0] for call in skill_view_mock.call_args_list] == ["blogwatcher", "find-nearby"]
|
||||
|
||||
prompt_arg = mock_agent.run_conversation.call_args.args[0]
|
||||
assert prompt_arg.index("blogwatcher") < prompt_arg.index("find-nearby")
|
||||
assert "Instructions for blogwatcher." in prompt_arg
|
||||
assert "Instructions for find-nearby." in prompt_arg
|
||||
assert "Combine the results." in prompt_arg
|
||||
|
|
|
|||
107
tests/hermes_cli/test_cron.py
Normal file
107
tests/hermes_cli/test_cron.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"""Tests for hermes_cli.cron command handling."""
|
||||
|
||||
from argparse import Namespace
|
||||
|
||||
import pytest
|
||||
|
||||
from cron.jobs import create_job, get_job, list_jobs
|
||||
from hermes_cli.cron import cron_command
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tmp_cron_dir(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("cron.jobs.CRON_DIR", tmp_path / "cron")
|
||||
monkeypatch.setattr("cron.jobs.JOBS_FILE", tmp_path / "cron" / "jobs.json")
|
||||
monkeypatch.setattr("cron.jobs.OUTPUT_DIR", tmp_path / "cron" / "output")
|
||||
return tmp_path
|
||||
|
||||
|
||||
class TestCronCommandLifecycle:
|
||||
def test_pause_resume_run(self, tmp_cron_dir, capsys):
|
||||
job = create_job(prompt="Check server status", schedule="every 1h")
|
||||
|
||||
cron_command(Namespace(cron_command="pause", job_id=job["id"]))
|
||||
paused = get_job(job["id"])
|
||||
assert paused["state"] == "paused"
|
||||
|
||||
cron_command(Namespace(cron_command="resume", job_id=job["id"]))
|
||||
resumed = get_job(job["id"])
|
||||
assert resumed["state"] == "scheduled"
|
||||
|
||||
cron_command(Namespace(cron_command="run", job_id=job["id"]))
|
||||
triggered = get_job(job["id"])
|
||||
assert triggered["state"] == "scheduled"
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Paused job" in out
|
||||
assert "Resumed job" in out
|
||||
assert "Triggered job" in out
|
||||
|
||||
def test_edit_can_replace_and_clear_skills(self, tmp_cron_dir, capsys):
|
||||
job = create_job(
|
||||
prompt="Combine skill outputs",
|
||||
schedule="every 1h",
|
||||
skill="blogwatcher",
|
||||
)
|
||||
|
||||
cron_command(
|
||||
Namespace(
|
||||
cron_command="edit",
|
||||
job_id=job["id"],
|
||||
schedule="every 2h",
|
||||
prompt="Revised prompt",
|
||||
name="Edited Job",
|
||||
deliver=None,
|
||||
repeat=None,
|
||||
skill=None,
|
||||
skills=["find-nearby", "blogwatcher"],
|
||||
clear_skills=False,
|
||||
)
|
||||
)
|
||||
updated = get_job(job["id"])
|
||||
assert updated["skills"] == ["find-nearby", "blogwatcher"]
|
||||
assert updated["name"] == "Edited Job"
|
||||
assert updated["prompt"] == "Revised prompt"
|
||||
assert updated["schedule_display"] == "every 120m"
|
||||
|
||||
cron_command(
|
||||
Namespace(
|
||||
cron_command="edit",
|
||||
job_id=job["id"],
|
||||
schedule=None,
|
||||
prompt=None,
|
||||
name=None,
|
||||
deliver=None,
|
||||
repeat=None,
|
||||
skill=None,
|
||||
skills=None,
|
||||
clear_skills=True,
|
||||
)
|
||||
)
|
||||
cleared = get_job(job["id"])
|
||||
assert cleared["skills"] == []
|
||||
assert cleared["skill"] is None
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Updated job" in out
|
||||
|
||||
def test_create_with_multiple_skills(self, tmp_cron_dir, capsys):
|
||||
cron_command(
|
||||
Namespace(
|
||||
cron_command="create",
|
||||
schedule="every 1h",
|
||||
prompt="Use both skills",
|
||||
name="Skill combo",
|
||||
deliver=None,
|
||||
repeat=None,
|
||||
skill=None,
|
||||
skills=["blogwatcher", "find-nearby"],
|
||||
)
|
||||
)
|
||||
out = capsys.readouterr().out
|
||||
assert "Created job" in out
|
||||
|
||||
jobs = list_jobs()
|
||||
assert len(jobs) == 1
|
||||
assert jobs[0]["skills"] == ["blogwatcher", "find-nearby"]
|
||||
assert jobs[0]["name"] == "Skill combo"
|
||||
|
|
@ -245,3 +245,35 @@ class TestUnifiedCronjobTool:
|
|||
|
||||
listing = json.loads(cronjob(action="list"))
|
||||
assert listing["jobs"][0]["skill"] == "blogwatcher"
|
||||
|
||||
def test_create_multi_skill_job(self):
|
||||
result = json.loads(
|
||||
cronjob(
|
||||
action="create",
|
||||
skills=["blogwatcher", "find-nearby"],
|
||||
prompt="Use both skills and combine the result.",
|
||||
schedule="every 1h",
|
||||
name="Combo job",
|
||||
)
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert result["skills"] == ["blogwatcher", "find-nearby"]
|
||||
|
||||
listing = json.loads(cronjob(action="list"))
|
||||
assert listing["jobs"][0]["skills"] == ["blogwatcher", "find-nearby"]
|
||||
|
||||
def test_update_can_clear_skills(self):
|
||||
created = json.loads(
|
||||
cronjob(
|
||||
action="create",
|
||||
skills=["blogwatcher", "find-nearby"],
|
||||
prompt="Use both skills and combine the result.",
|
||||
schedule="every 1h",
|
||||
)
|
||||
)
|
||||
updated = json.loads(
|
||||
cronjob(action="update", job_id=created["job_id"], skills=[])
|
||||
)
|
||||
assert updated["success"] is True
|
||||
assert updated["job"]["skills"] == []
|
||||
assert updated["job"]["skill"] is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue