253 lines
8.8 KiB
Python
Raw Normal View History

2026-07-26 14:57:18 +02:00
# retoor <retoor@molodetz.nl>
import pytest
2026-07-26 16:45:38 +02:00
from devplacepy.database import invalidate_admins_cache, set_setting
2026-07-26 14:57:18 +02:00
from devplacepy.services.devii.errors import ToolInputError
2026-07-26 16:45:38 +02:00
from devplacepy.services.devii.tasks import limits
from devplacepy.services.devii.tasks.context import task_run_scope
2026-07-26 14:57:18 +02:00
from devplacepy.services.devii.tasks.controller import TaskController
2026-07-26 16:45:38 +02:00
from devplacepy.services.devii.tasks.guards import (
REASON_CREATE_QUOTA,
REASON_NESTED,
REASON_NOT_A_USER,
AutomationDenied,
)
2026-07-26 14:57:18 +02:00
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,
2026-07-26 16:45:38 +02:00
"created_at": to_iso(now_utc()),
2026-07-26 14:57:18 +02:00
}
)
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
2026-07-26 16:45:38 +02:00
def _spend_creation_quota(store, count):
for _ in range(count):
store.create(_record())
def test_member_can_create_a_task(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Member"))
record = _record()
store.create(record)
assert store.get(record["uid"]) is not None
def test_member_creation_stops_at_the_daily_limit(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Member"))
_spend_creation_quota(store, limits.DEFAULT_MEMBER_CREATE)
with pytest.raises(AutomationDenied) as denied:
store.create(_record())
assert denied.value.reason == REASON_CREATE_QUOTA
assert denied.value.retry_at is not None
def test_deleting_a_task_does_not_give_the_creation_slot_back(local_db):
2026-07-26 14:57:18 +02:00
store = TaskStore(local_db, "user", _account(local_db, "Member"))
2026-07-26 16:45:38 +02:00
records = [_record() for _ in range(limits.DEFAULT_MEMBER_CREATE)]
for record in records:
store.create(record)
for record in records:
store.delete(record["uid"])
2026-07-26 14:57:18 +02:00
with pytest.raises(AutomationDenied):
store.create(_record())
2026-07-26 16:45:38 +02:00
def test_administrator_creation_stops_at_its_own_limit(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Admin"))
_spend_creation_quota(store, limits.DEFAULT_ADMIN_CREATE)
with pytest.raises(AutomationDenied) as denied:
store.create(_record())
assert denied.value.reason == REASON_CREATE_QUOTA
def test_creation_limit_is_configurable(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Member"))
set_setting(limits.FIELD_MEMBER_CREATE, "1")
try:
store.create(_record())
with pytest.raises(AutomationDenied):
store.create(_record())
finally:
set_setting(limits.FIELD_MEMBER_CREATE, str(limits.DEFAULT_MEMBER_CREATE))
def test_guest_cannot_persist_a_task(local_db):
store = TaskStore(local_db, "guest", "guest-cookie")
with pytest.raises(AutomationDenied) as denied:
store.create(_record())
assert denied.value.reason == REASON_NOT_A_USER
def test_member_cannot_create_a_task_from_inside_a_task_run(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Member"))
with task_run_scope():
with pytest.raises(AutomationDenied) as denied:
store.create(_record())
assert denied.value.reason == REASON_NESTED
def test_administrator_can_create_a_task_from_inside_a_task_run(local_db):
2026-07-26 14:57:18 +02:00
store = TaskStore(local_db, "user", _account(local_db, "Admin"))
record = _record()
2026-07-26 16:45:38 +02:00
with task_run_scope():
store.create(record)
2026-07-26 14:57:18 +02:00
assert store.get(record["uid"]) is not None
2026-07-26 16:45:38 +02:00
def test_member_cannot_enable_a_task_from_inside_a_task_run(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Member"))
record = _record()
store.create(record)
store.update(record["uid"], {"enabled": False, "status": "disabled"})
with task_run_scope():
with pytest.raises(AutomationDenied) as denied:
store.update(record["uid"], {"enabled": True})
assert denied.value.reason == REASON_NESTED
def test_administrator_can_enable_a_task_from_inside_a_task_run(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Admin"))
2026-07-26 14:57:18 +02:00
record = _record()
2026-07-26 16:45:38 +02:00
store.create(record)
store.update(record["uid"], {"enabled": False, "status": "disabled"})
with task_run_scope():
store.update(record["uid"], {"enabled": True})
assert store.get(record["uid"])["enabled"]
2026-07-26 14:57:18 +02:00
2026-07-26 16:45:38 +02:00
def test_disabling_from_inside_a_task_run_is_always_allowed(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Member"))
2026-07-26 14:57:18 +02:00
record = _record()
store.create(record)
2026-07-26 16:45:38 +02:00
with task_run_scope():
store.update(record["uid"], {"enabled": False, "status": "disabled"})
assert not store.get(record["uid"])["enabled"]
def test_local_operator_store_bypasses_every_gate(local_db):
store = TaskStore(local_db, "user", "cli", operator=True)
with task_run_scope():
for _ in range(limits.DEFAULT_MEMBER_CREATE + 3):
store.create(_record())
assert store.count_active() >= limits.DEFAULT_MEMBER_CREATE
2026-07-26 14:57:18 +02:00
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):
2026-07-26 16:45:38 +02:00
store = TaskStore(local_db, "user", _account(local_db, "Admin"))
2026-07-26 14:57:18 +02:00
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}
)
2026-07-26 16:45:38 +02:00
def test_controller_reports_when_the_next_creation_slot_frees(local_db):
2026-07-26 14:57:18 +02:00
store = TaskStore(local_db, "user", _account(local_db, "Member"))
controller = TaskController(store)
2026-07-26 16:45:38 +02:00
for _ in range(limits.DEFAULT_MEMBER_CREATE):
controller.create_task(
{"prompt": "work", "kind": "interval", "every_seconds": 900}
)
with pytest.raises(ToolInputError) as failure:
2026-07-26 14:57:18 +02:00
controller.create_task(
{"prompt": "work", "kind": "interval", "every_seconds": 900}
)
2026-07-26 16:45:38 +02:00
assert REASON_CREATE_QUOTA in str(failure.value)
assert "frees up at" in str(failure.value)
def test_controller_refuses_nested_creation_for_a_member(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Member"))
controller = TaskController(store)
with task_run_scope():
with pytest.raises(ToolInputError) as failure:
controller.create_task(
{"prompt": "work", "kind": "interval", "every_seconds": 900}
)
assert REASON_NESTED in str(failure.value)
def test_controller_refuses_nested_run_now_for_a_member(local_db):
store = TaskStore(local_db, "user", _account(local_db, "Member"))
controller = TaskController(store)
created = controller.create_task(
{"prompt": "work", "kind": "interval", "every_seconds": 900}
)
import json
uid = json.loads(created)["task"]["uid"]
with task_run_scope():
with pytest.raises(ToolInputError) as failure:
controller.run_task_now({"uid": uid})
assert REASON_NESTED in str(failure.value)