112 lines
3.5 KiB
Python
112 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
|
|
from opensquilla.scheduler.persistence import JobStore
|
|
from opensquilla.scheduler.types import CronJob, JobReservation, ScheduleKind
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduler_persistence_round_trips_tool_policy(tmp_path) -> None:
|
|
store = JobStore(str(tmp_path / "scheduler.db"))
|
|
await store.open()
|
|
try:
|
|
job = CronJob(
|
|
id="policy",
|
|
name="Policy",
|
|
cron_expr="*/5 * * * *",
|
|
schedule_raw="*/5 * * * *",
|
|
handler_key="agent_run",
|
|
tool_policy={
|
|
"profile": "minimal",
|
|
"also_allow": ["memory_search"],
|
|
"deny": ["web_fetch"],
|
|
},
|
|
)
|
|
|
|
await store.save(job)
|
|
loaded = await store.get("policy")
|
|
finally:
|
|
await store.close()
|
|
|
|
assert loaded is not None
|
|
assert loaded.tool_policy == {
|
|
"profile": "minimal",
|
|
"also_allow": ["memory_search"],
|
|
"deny": ["web_fetch"],
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_due_checks_treat_offset_timestamps_as_absolute_time(tmp_path) -> None:
|
|
store = JobStore(str(tmp_path / "scheduler.db"))
|
|
await store.open()
|
|
try:
|
|
job = CronJob(
|
|
id="offset-at",
|
|
name="Offset AT",
|
|
cron_expr="2026-05-25T16:15:00+08:00",
|
|
schedule_kind=ScheduleKind.AT,
|
|
schedule_raw="2026-05-25T16:15:00+08:00",
|
|
handler_key="static_message",
|
|
next_run_at=datetime.fromisoformat("2026-05-25T16:15:00+08:00"),
|
|
delete_after_run=True,
|
|
)
|
|
await store.save(job)
|
|
|
|
now = datetime.fromisoformat("2026-05-25T08:19:31+00:00")
|
|
due_jobs = [due_job async for due_job in store.iter_due(now)]
|
|
reservation = await store.reserve_due_job("offset-at", now)
|
|
finally:
|
|
await store.close()
|
|
|
|
assert [due_job.id for due_job in due_jobs] == ["offset-at"]
|
|
assert isinstance(reservation, JobReservation)
|
|
assert reservation.job.id == "offset-at"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_open_normalizes_existing_offset_timestamps_for_due_checks(tmp_path) -> None:
|
|
db_path = tmp_path / "scheduler.db"
|
|
store = JobStore(str(db_path))
|
|
await store.open()
|
|
await store.close()
|
|
|
|
with sqlite3.connect(db_path) as conn:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO scheduler_jobs
|
|
(id, name, cron_expr, handler_key, created_at, updated_at,
|
|
next_run_at, schedule_kind, schedule_raw, delete_after_run)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
"legacy-offset-at",
|
|
"Legacy Offset AT",
|
|
"2026-05-25T16:15:00+08:00",
|
|
"static_message",
|
|
"2026-05-25T08:12:36.504054+00:00",
|
|
"2026-05-25T08:12:36.508732+00:00",
|
|
"2026-05-25T16:15:00+08:00",
|
|
"at",
|
|
"2026-05-25T16:15:00+08:00",
|
|
1,
|
|
),
|
|
)
|
|
|
|
store = JobStore(str(db_path))
|
|
await store.open()
|
|
try:
|
|
now = datetime.fromisoformat("2026-05-25T08:19:31+00:00")
|
|
loaded = await store.get("legacy-offset-at")
|
|
due_jobs = [due_job async for due_job in store.iter_due(now)]
|
|
finally:
|
|
await store.close()
|
|
|
|
assert loaded is not None
|
|
assert loaded.next_run_at == datetime.fromisoformat("2026-05-25T08:15:00+00:00")
|
|
assert [due_job.id for due_job in due_jobs] == ["legacy-offset-at"]
|