stage-8: cron scheduler with APScheduler
Scheduler class, cron/jobs.json, Discord /cron commands, CLI cron subcommand, job lifecycle management. 88 new tests (281 total). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -364,3 +364,124 @@ class TestSend:
|
||||
with pytest.raises(SystemExit):
|
||||
cli.cmd_send(_args(alias="nope", message=["hi"]))
|
||||
assert "unknown channel" in capsys.readouterr().out.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cron list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCronList:
|
||||
def test_list_empty(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = []
|
||||
mock_sched.list_jobs.return_value = []
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
cli._cron_list()
|
||||
assert "No scheduled jobs" in capsys.readouterr().out
|
||||
|
||||
def test_list_shows_table(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = [
|
||||
{
|
||||
"name": "daily-run",
|
||||
"cron": "30 6 * * *",
|
||||
"channel": "work",
|
||||
"model": "sonnet",
|
||||
"enabled": True,
|
||||
"last_status": "ok",
|
||||
"next_run": None,
|
||||
}
|
||||
]
|
||||
mock_sched.list_jobs.return_value = mock_sched._load_jobs.return_value
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
cli._cron_list()
|
||||
out = capsys.readouterr().out
|
||||
assert "daily-run" in out
|
||||
assert "30 6 * * *" in out
|
||||
assert "sonnet" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cron add / remove / enable / disable
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCronAdd:
|
||||
def test_add_success(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = []
|
||||
mock_sched.add_job.return_value = {
|
||||
"name": "new-job",
|
||||
"cron": "0 * * * *",
|
||||
"channel": "ch",
|
||||
"model": "sonnet",
|
||||
}
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
cli._cron_add("new-job", "0 * * * *", "ch", "hello prompt", "sonnet", [])
|
||||
out = capsys.readouterr().out
|
||||
assert "new-job" in out
|
||||
assert "added" in out.lower()
|
||||
|
||||
def test_add_error(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = []
|
||||
mock_sched.add_job.side_effect = ValueError("duplicate name")
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
with pytest.raises(SystemExit):
|
||||
cli._cron_add("dup", "0 * * * *", "ch", "prompt", "sonnet", [])
|
||||
assert "duplicate name" in capsys.readouterr().out
|
||||
|
||||
|
||||
class TestCronRemove:
|
||||
def test_remove_found(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = []
|
||||
mock_sched.remove_job.return_value = True
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
cli._cron_remove("old-job")
|
||||
assert "removed" in capsys.readouterr().out.lower()
|
||||
|
||||
def test_remove_not_found(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = []
|
||||
mock_sched.remove_job.return_value = False
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
cli._cron_remove("ghost")
|
||||
assert "not found" in capsys.readouterr().out.lower()
|
||||
|
||||
|
||||
class TestCronEnable:
|
||||
def test_enable_found(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = []
|
||||
mock_sched.enable_job.return_value = True
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
cli._cron_enable("my-job")
|
||||
assert "enabled" in capsys.readouterr().out.lower()
|
||||
|
||||
def test_enable_not_found(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = []
|
||||
mock_sched.enable_job.return_value = False
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
cli._cron_enable("nope")
|
||||
assert "not found" in capsys.readouterr().out.lower()
|
||||
|
||||
|
||||
class TestCronDisable:
|
||||
def test_disable_found(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = []
|
||||
mock_sched.disable_job.return_value = True
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
cli._cron_disable("my-job")
|
||||
assert "disabled" in capsys.readouterr().out.lower()
|
||||
|
||||
def test_disable_not_found(self, iso, capsys):
|
||||
mock_sched = MagicMock()
|
||||
mock_sched._load_jobs.return_value = []
|
||||
mock_sched.disable_job.return_value = False
|
||||
with patch("src.scheduler.Scheduler", return_value=mock_sched):
|
||||
cli._cron_disable("nope")
|
||||
assert "not found" in capsys.readouterr().out.lower()
|
||||
|
||||
Reference in New Issue
Block a user