search · graph · sql тестируем
как вызовы примитивов Knowledge Store под ACL личности
владельца: правильный инструмент выбран по намерению, права режут на
стороне KS. Саму выдачу, ранжирование и слияние держит и проверяет
Knowledge Store — здесь не дублируем.
tokens_used, два замка (выключатель владельца
enabled и липкий admin_paused) плюс
условие модели (model_id IS NOT NULL; снятие из списка →
SET NULL → стоп), деактивация учётки →
enabled=false агентам, запись
skipped на рантайм-отказе (budget_exceeded /
already_running) и молчание планировщика при
закрытом замке, FK-каскады от агента и от учётки. Здесь же —
что наружу это отражается ровными кодами, не 500.
search / graph /
sql
next_run_at, окно
недельного бюджета
agents / agent_runs),
SAQ-обработчик вызовом напрямую — без поднятия воркера
@pytest.mark.unit / @pytest.mark.integration —
по типу; @pytest.mark.p0 / p1 — по
приоритету, ортогонально типу
failed с reason=iteration_cap (упёрся в потолок, не отклонён до старта), не бесконечный вызов моделиsearch · graph · sql поднимаются по имени tool-call'а и зовут соответствующий примитив Knowledge Storesearch · graph · sql в набор, хотя оно locked; «locked» = не отключается вручную, не «есть при любом состоянии базы»; агент стартует и деградирует на внешних инструментах либо честно сообщает об отсутствии данных — пустая база в гейт готовности не входитqueued → running → succeeded; output записан в строку прогонаfailed, error заполнен; отличается от skippedtokens_used проставлен по факту прогона — основа недельной суммы бюджетаstarted_at / finished_at проставлены на старте и завершении; created_at / updated_at — server_default + триггерskipped) не имеет started_at — он отклонён до запуска, не оборван в работеUNIQUE (agent_id) WHERE state IN ('queued','running') — гонку (ручной запуск совпал с наступившим слотом) гасит сама база, не только проверка в кодеrunning → по протуханию heartbeat_at прогон жнётся в failed с reason=stale, замок отпускается, агент стартует снова — зомби его не запираетSUM(tokens_used) по всем агентам владельца за текущую неделю (по finished_at), а не на агентаskipped с reason=budget_exceeded, tokens_used=0platform_settings, в модуле не хранится — меняется централизованноschedule NULL → агент запускается лишь вручную, планировщик его не трогаетnext_run_at от прошлого запускаHH:MM — даёт верный next_run_atHH:MM резолвится против users.timezone владельца (при NULL — org-default platform_settings.timezone), затем хранится UTCnext_run_at ставит прогон в фоновую очередь с trigger=scheduledrunning/queued прогона нового цикла не создаёт — строка skipped, reason=already_runningtrigger=manual) расписания не требует — работает и при schedule=NULL; замки (enabled, admin_paused) при этом всё равно чтятсяadmin_paused не может — только администратор; попытка владельца не меняет флагenabled, не admin_paused, бюджете в пределах И model_id IS NOT NULL; оси независимыagent_models → ON DELETE SET NULL обнуляет agents.model_id → агента нет в скане (next_run_at обнулён), запуск недоступен, записи skipped нет — как при закрытом замке; на дефолт не откатываетсяenabled=false или admin_paused=true агента нет в скане планировщика (next_run_at обнулён) и кнопка ручного запуска недоступна — прогон не идёт и записи skipped не появляется; durable-стоп журнала не копитenabled всем его агентам; реактивация флаг не возвращает — оживляет только сам владелец вручнуюagents и за ними agent_runs уходят по ON DELETE CASCADEadmin_pausedenabledmodel_id из разрешённого списка сохраняется и применяется в прогоне; при создании поле проставляется (преднабор — дефолт списка)model_id вне agent_models (или embedding-тип) → 422, не 500202 + id прогона; создан agent_run (trigger=manual, queued), задача в SAQPOST /agents/{id}/run, пока прогон агента уже running → 409 CONFLICT (single-flight, не задваивает прогон); согласовано с доменным already_running → skipped{id} → 404, не 500POST /run не стартует прогон: 409 (или доменный skipped с причиной budget_exceeded) — зафиксирован HTTP-исход, не 500trigger / state не из формы, пустое name → 422, не 500schedule — размеченное объединение по type (interval / calendar), weekday 0–6, time HH:MM; кривой на входе отбит валидацией: (а) неизвестный type; (б) calendar без time; (в) weekday вне 0–6 → 422, не 500 → Форма расписанияoutput, tokens_used и исходом; skipped с reason отдаётся отлично от failedenabled, read-only для админа), и admin_paused — сводный статус строки виден админуadmin_paused на любом агенте; выключатель enabled владельца не трогаетadmin_paused → 403403 FORBIDDEN401 UNAUTHORIZED (параметризовано по всем роутам)401
Разделение задаётся назначением, не папками. unit/ идёт на
каждом PR детерминированно — поддельная модель и фейковые примитивы,
без сети и БД. integration/ и api/ — отдельным,
более редким шагом на поднятой БД: первый проверяет домен (прогоны,
бюджет, контроль, полоса), второй — HTTP-контракт на ASGI-приложении.
Реальная модель и провайдер остаются вне CI, ручной отладкой.
Приоритет (P0–P1) ортогонален каталогам и задаётся маркерами
(pytest -m p0).
tests/agent_engine/каталог модуля
conftest.pyфейковый chat-client · фейковые инструменты search/graph/sql · time-machine · фабрики agents/agent_runs · SAQ напрямуюunit/цикл и инструменты на поддельной модели — каждый PR, детерминированно
test_agent_loop.py · test_prompt_composition.pythink→tool→observe, слойная сборка промтаtest_tools.pysearch/graph/sql как вызовы примитивов KS под ACL личностиintegration/домен на поднятой БД
test_run_lifecycle.pyсостояния прогона, замок одного активного + жатва stale по heartbeat, учёт токенов, timestampstest_budget · test_scheduling · test_governance · test_isolationнедельный бюджет, расписание, замки и каскады, фоновая полосаapi/HTTP-контракт эндпоинтов — httpx + ASGI + БД
test_api_agents · _admin · _accessCRUD и запуск, админский реестр и замок, роли и аутентификация