137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
|
|
# retoor <retoor@molodetz.nl>
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from devplacepy.database import invalidate_admins_cache
|
||
|
|
from devplacepy.services.devii.errors import ToolInputError
|
||
|
|
from devplacepy.services.devii.tasks.controller import TaskController
|
||
|
|
from devplacepy.services.devii.tasks.guards import AutomationDenied
|
||
|
|
from devplacepy.services.devii.tasks.schedule import now_utc, to_iso
|
||
|
|
from devplacepy.services.devii.tasks.store import TaskStore, claim, due_rows
|
||
|
|
from devplacepy.utils import generate_uid
|
||
|
|
|
||
|
|
|
||
|
|
def _account(local_db, role):
|
||
|
|
uid = generate_uid()
|
||
|
|
local_db["users"].insert(
|
||
|
|
{
|
||
|
|
"uid": uid,
|
||
|
|
"username": f"store-{uid[-10:]}",
|
||
|
|
"role": role,
|
||
|
|
"deleted_at": None,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
invalidate_admins_cache()
|
||
|
|
return uid
|
||
|
|
|
||
|
|
|
||
|
|
def _record(**overrides):
|
||
|
|
record = {
|
||
|
|
"uid": generate_uid(),
|
||
|
|
"prompt": "work",
|
||
|
|
"enabled": True,
|
||
|
|
"status": "pending",
|
||
|
|
"created_at": to_iso(now_utc()),
|
||
|
|
"next_run_at": to_iso(now_utc()),
|
||
|
|
"run_count": 0,
|
||
|
|
"failure_count": 0,
|
||
|
|
"kind": "interval",
|
||
|
|
"every_seconds": 900,
|
||
|
|
"max_runs": 10,
|
||
|
|
}
|
||
|
|
record.update(overrides)
|
||
|
|
return record
|
||
|
|
|
||
|
|
|
||
|
|
def test_member_cannot_persist_a_task(local_db):
|
||
|
|
store = TaskStore(local_db, "user", _account(local_db, "Member"))
|
||
|
|
with pytest.raises(AutomationDenied):
|
||
|
|
store.create(_record())
|
||
|
|
|
||
|
|
|
||
|
|
def test_administrator_can_persist_a_task(local_db):
|
||
|
|
store = TaskStore(local_db, "user", _account(local_db, "Admin"))
|
||
|
|
record = _record()
|
||
|
|
store.create(record)
|
||
|
|
assert store.get(record["uid"]) is not None
|
||
|
|
|
||
|
|
|
||
|
|
def test_member_cannot_re_enable_a_task(local_db):
|
||
|
|
admin = _account(local_db, "Admin")
|
||
|
|
member = _account(local_db, "Member")
|
||
|
|
admin_store = TaskStore(local_db, "user", admin)
|
||
|
|
record = _record()
|
||
|
|
admin_store.create(record)
|
||
|
|
local_db["devii_tasks"].update(
|
||
|
|
{"uid": record["uid"], "owner_id": member, "enabled": False}, ["uid"]
|
||
|
|
)
|
||
|
|
member_store = TaskStore(local_db, "user", member)
|
||
|
|
with pytest.raises(AutomationDenied):
|
||
|
|
member_store.update(record["uid"], {"enabled": True})
|
||
|
|
member_store.update(record["uid"], {"enabled": False, "status": "disabled"})
|
||
|
|
|
||
|
|
|
||
|
|
def test_local_operator_store_bypasses_the_role_gate(local_db):
|
||
|
|
store = TaskStore(local_db, "user", "cli", operator=True)
|
||
|
|
record = _record()
|
||
|
|
store.create(record)
|
||
|
|
assert store.get(record["uid"]) is not None
|
||
|
|
|
||
|
|
|
||
|
|
def test_claim_succeeds_once(local_db):
|
||
|
|
store = TaskStore(local_db, "user", _account(local_db, "Admin"))
|
||
|
|
record = _record()
|
||
|
|
store.create(record)
|
||
|
|
assert claim(local_db, record["uid"]) is True
|
||
|
|
assert claim(local_db, record["uid"]) is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_claim_refuses_a_disabled_task(local_db):
|
||
|
|
store = TaskStore(local_db, "user", _account(local_db, "Admin"))
|
||
|
|
record = _record()
|
||
|
|
store.create(record)
|
||
|
|
store.update(record["uid"], {"enabled": False, "status": "disabled"})
|
||
|
|
assert claim(local_db, record["uid"]) is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_due_rows_skips_future_and_running_tasks(local_db):
|
||
|
|
admin = _account(local_db, "Admin")
|
||
|
|
store = TaskStore(local_db, "user", admin)
|
||
|
|
now = now_utc()
|
||
|
|
due = _record(next_run_at=to_iso(now.replace(year=now.year - 1)))
|
||
|
|
future = _record(next_run_at=to_iso(now.replace(year=now.year + 1)))
|
||
|
|
running = _record(
|
||
|
|
status="running", next_run_at=to_iso(now.replace(year=now.year - 1))
|
||
|
|
)
|
||
|
|
for record in (due, future, running):
|
||
|
|
store.create(record)
|
||
|
|
found = {row["uid"] for row in due_rows(local_db, to_iso(now), 500)}
|
||
|
|
assert due["uid"] in found
|
||
|
|
assert future["uid"] not in found
|
||
|
|
assert running["uid"] not in found
|
||
|
|
|
||
|
|
|
||
|
|
def test_controller_caps_active_tasks_per_owner(local_db, monkeypatch):
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"devplacepy.services.devii.tasks.controller.max_active_per_owner", lambda: 2
|
||
|
|
)
|
||
|
|
store = TaskStore(local_db, "user", _account(local_db, "Admin"))
|
||
|
|
controller = TaskController(store)
|
||
|
|
for _ in range(2):
|
||
|
|
controller.create_task(
|
||
|
|
{"prompt": "work", "kind": "interval", "every_seconds": 900}
|
||
|
|
)
|
||
|
|
with pytest.raises(ToolInputError):
|
||
|
|
controller.create_task(
|
||
|
|
{"prompt": "work", "kind": "interval", "every_seconds": 900}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_controller_refuses_a_member(local_db):
|
||
|
|
store = TaskStore(local_db, "user", _account(local_db, "Member"))
|
||
|
|
controller = TaskController(store)
|
||
|
|
with pytest.raises(ToolInputError):
|
||
|
|
controller.create_task(
|
||
|
|
{"prompt": "work", "kind": "interval", "every_seconds": 900}
|
||
|
|
)
|