- Add agents package: controller, runner, store, tools, actions, evals, policy - Wire agents into Devii service, session, registry, CLI, and dispatcher - Extend database schema and config for agent persistence - Add news and telegram bridge updates plus landing UI and docs - Cover agents, news, and telegram flows with unit tests
191 lines
6.6 KiB
Python
191 lines
6.6 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from devplacepy.database import get_setting, set_setting
|
|
from devplacepy.services.devii.agents.controller import AgentController
|
|
from devplacepy.services.devii.agents.policy import CATEGORIES
|
|
from devplacepy.services.devii.agents.store import AgentStore
|
|
from devplacepy.services.devii.config import FIELD_AGENTS_ENABLED
|
|
from devplacepy.services.devii.errors import ToolInputError
|
|
from devplacepy.utils import generate_uid
|
|
from tests.conftest import run_async
|
|
|
|
|
|
class _Quota:
|
|
def __init__(self, reached=False):
|
|
self.reached = reached
|
|
|
|
|
|
class _StubSession:
|
|
def __init__(self, db, owner_id):
|
|
self.store = AgentStore(db, "user", owner_id)
|
|
self.owner_kind = "user"
|
|
self.owner_id = owner_id
|
|
self.channel = "main"
|
|
self.emitted = []
|
|
self.quota = _Quota()
|
|
|
|
def _quota_snapshot(self):
|
|
return {"limit_reached": self.quota.reached}
|
|
|
|
async def _emit(self, payload, buffer=True):
|
|
self.emitted.append(payload)
|
|
return True
|
|
|
|
async def emit_agent(self, payload):
|
|
self.emitted.append(payload)
|
|
return True
|
|
|
|
|
|
class _StubSupervisor:
|
|
def __init__(self, store=None):
|
|
self._store = store
|
|
self.resumed = []
|
|
self.cancelled = []
|
|
|
|
async def resume(self, uid, answer):
|
|
self.resumed.append((uid, answer))
|
|
if self._store is not None:
|
|
self._store.update(uid, {"status": "running"})
|
|
return True
|
|
|
|
async def cancel(self, uid, reason):
|
|
self.cancelled.append((uid, reason))
|
|
if self._store is not None:
|
|
self._store.update(uid, {"status": "cancelled"})
|
|
return True
|
|
|
|
|
|
@pytest.fixture
|
|
def flag_on(local_db):
|
|
previous = get_setting(FIELD_AGENTS_ENABLED, "")
|
|
set_setting(FIELD_AGENTS_ENABLED, "1")
|
|
try:
|
|
yield
|
|
finally:
|
|
set_setting(FIELD_AGENTS_ENABLED, previous or "")
|
|
|
|
|
|
@pytest.fixture
|
|
def session(local_db):
|
|
return _StubSession(local_db, f"ctl-{generate_uid()}")
|
|
|
|
|
|
def _start_args(**overrides):
|
|
args = {
|
|
"category": "research",
|
|
"task": "Compare static site generators.",
|
|
"output_format": "A markdown table.",
|
|
}
|
|
args.update(overrides)
|
|
return args
|
|
|
|
|
|
def test_start_validates_contract(local_db, session, flag_on):
|
|
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("agent_start", _start_args(category="nope")))
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("agent_start", _start_args(task=" ")))
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("agent_start", _start_args(output_format="")))
|
|
with pytest.raises(ToolInputError):
|
|
run_async(
|
|
controller.dispatch(
|
|
"agent_start", _start_args(tools_extra=["missing_tool"])
|
|
)
|
|
)
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("agent_start", _start_args(tools_extra=["run_js"])))
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("agent_start", _start_args(budget_hint="huge")))
|
|
|
|
|
|
def test_start_queues_and_emits(local_db, session, flag_on):
|
|
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
|
|
raw = run_async(controller.dispatch(
|
|
"agent_start", _start_args(label="sg", budget_hint="quick"))
|
|
)
|
|
payload = json.loads(raw)
|
|
assert payload["status"] == "queued"
|
|
assert payload["run"]["category"] == "research"
|
|
assert session.emitted[0]["type"] == "agent"
|
|
assert session.emitted[0]["kind"] == "started"
|
|
|
|
|
|
def test_start_refuses_when_flag_off_or_quota_spent(local_db, session):
|
|
set_setting(FIELD_AGENTS_ENABLED, "")
|
|
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("agent_start", _start_args()))
|
|
set_setting(FIELD_AGENTS_ENABLED, "1")
|
|
try:
|
|
session.quota.reached = True
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("agent_start", _start_args()))
|
|
finally:
|
|
set_setting(FIELD_AGENTS_ENABLED, "")
|
|
|
|
|
|
def test_start_enforces_active_cap(local_db, session, flag_on):
|
|
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
|
|
run_async(controller.dispatch("agent_start", _start_args()))
|
|
run_async(controller.dispatch("agent_start", _start_args()))
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("agent_start", _start_args()))
|
|
|
|
|
|
def test_status_result_cancel_list_round_trip(local_db, session, flag_on):
|
|
supervisor = _StubSupervisor(session.store)
|
|
controller = AgentController(session, supervisor=supervisor)
|
|
uid = json.loads(run_async(controller.dispatch("agent_start", _start_args())))["run"][
|
|
"uid"
|
|
]
|
|
status = json.loads(run_async(controller.dispatch("agent_status", {"uid": uid})))
|
|
assert status["run"]["status"] == "queued"
|
|
listed = json.loads(run_async(controller.dispatch("agent_list", {})))
|
|
assert listed["count"] == 1
|
|
cancelled = json.loads(run_async(controller.dispatch("agent_cancel", {"uid": uid})))
|
|
assert cancelled["status"] == "cancelled"
|
|
assert supervisor.cancelled == [(uid, "cancelled by orchestrator")]
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("agent_status", {"uid": "missing"}))
|
|
|
|
|
|
def test_result_resumes_needs_user(local_db, session, flag_on):
|
|
supervisor = _StubSupervisor(session.store)
|
|
controller = AgentController(session, supervisor=supervisor)
|
|
uid = json.loads(run_async(controller.dispatch("agent_start", _start_args())))["run"][
|
|
"uid"
|
|
]
|
|
controller._store.update(uid, {"status": "needs_user", "needs_question": "Budget?"})
|
|
payload = json.loads(
|
|
run_async(controller.dispatch("agent_result", {"uid": uid, "answer": "zero"}))
|
|
)
|
|
assert supervisor.resumed == [(uid, "zero")]
|
|
assert payload["run"]["uid"] == uid
|
|
|
|
|
|
def test_ask_user_returns_pause_marker(session):
|
|
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
|
|
payload = json.loads(
|
|
run_async(controller.dispatch("ask_user", {"question": "Which budget?"}))
|
|
)
|
|
assert payload["status"] == "paused_for_user"
|
|
with pytest.raises(ToolInputError):
|
|
run_async(controller.dispatch("ask_user", {"question": " "}))
|
|
|
|
|
|
def test_categories_match_policy():
|
|
assert set(CATEGORIES) == {
|
|
"research",
|
|
"files",
|
|
"planning-report",
|
|
"email-triage",
|
|
"containers",
|
|
"long-mutation",
|
|
}
|