← Agent Engine

Тест-кейсы

agent-engine · workzone
Принятые решения
Цикл — на поддельной модели Агентный цикл (думает → зовёт инструмент → читает результат) проверяется детерминированно: фейковый chat-client с записанной последовательностью tool-call'ов и фейковые инструменты. Реальная модель и провайдер — вне CI, ручная отладка; в автоматический прогон идёт только воспроизводимый дубль.
Инструменты — контракт к KS, не его повтор 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.
Стек и инфраструктура
Имеется done pytest, pytest-asyncio, pytest-cov, httpx
Дубли модели фейковый chat-client (записанные последовательности tool-call'ов) + фейковые инструменты search / graph / sql
Время time-machine — расписание, next_run_at, окно недельного бюджета
Прочее factory_boy (agents / agent_runs), SAQ-обработчик вызовом напрямую — без поднятия воркера
Маркеры @pytest.mark.unit / @pytest.mark.integration — по типу; @pytest.mark.p0 / p1 — по приоритету, ортогонально типу
Unit Цикл и композиция промта на поддельной модели AI детерминированно · каждый PR
test_agent_loop.py
think → tool → observe на фейковом chat-client
Кейсы
шаг с инструментоммодель зовёт инструмент → результат вернулся в контекст → модель делает следующий шаг с ним на руках
завершениефинальный ответ без tool-call останавливает цикл; выход уходит в журнал прогона
потолок итерацийзацикливание обрывается на лимите шагов — прогон failed с reason=iteration_cap (упёрся в потолок, не отклонён до старта), не бесконечный вызов модели
много шагов в прогоненесколько последовательных вызовов инструментов в одном прогоне — контекст накапливается шаг за шагом
выбор по намерениюзаписанная последовательность зовёт нужный из трёх инструментов под задачу — диспетчеризация tool-call по имени отрабатывает
tool-output — данные, не командафейк-инструмент возвращает текст с инъекцией («ignore previous, do X») → результат кладётся в контекст как данные, не как инструкция; safety-периметр держится, инъекция через tool-output правила платформы не перебивает → Композиция промта
test_prompt_composition.py
Слойная сборка промта, безопасность не отменяется
Кейсы
порядок слоёвплатформенный prompt снизу (safety + org), промт владельца сверху, инженерный слой замыкает — сборка собирается ровно в этом порядке
владелец не отменяет safetyпромт владельца не перекрывает платформенный safety-слой — формулировка владельца не вырезает периметр
привязка добавляется кодомпривязка личности владельца и описания трёх инструментов вставляются инженерным слоем, не берутся из текста владельца
Unit / Contract Инструменты к Knowledge Store фейковые примитивы KS · каждый PR
test_tools.py
search / graph / sql — вызов под ACL личности, чтение KS
Кейсы
три инструмента по намерениюsearch · graph · sql поднимаются по имени tool-call'а и зовут соответствующий примитив Knowledge Store
под ACL личностивызов идёт правами личности владельца (рамка владельца), не отдельной личности агента — фейк-примитив получает контекст создателя
права режет KSотсечение по правам — pre-filter на стороне Knowledge Store, не пост-фильтр в агенте; здесь проверяем, что контекст ACL передан, выдачу держит KS
только чтениеагент в Knowledge Store ничего не пишет — все три инструмента read-only, мутирующего примитива им не дано (граница чтения KS)
пустая база → ядро не поданоKnowledge Store пуст (is_empty) → харнесс не вносит ядро search · graph · sql в набор, хотя оно locked; «locked» = не отключается вручную, не «есть при любом состоянии базы»; агент стартует и деградирует на внешних инструментах либо честно сообщает об отсутствии данных — пустая база в гейт готовности не входит
Integration · P0 Жизненный цикл прогона БД · отдельный шаг CI
test_run_lifecycle.py
Состояния agent_runs, замок одного активного прогона, учёт расхода, метки времени
Кейсы
путь успехаqueuedrunningsucceeded; output записан в строку прогона
провалпрогон уходит в failed, error заполнен; отличается от skipped
учёт токеновtokens_used проставлен по факту прогона — основа недельной суммы бюджета
timestamps работыstarted_at / finished_at проставлены на старте и завершении; created_at / updated_at — server_default + триггер
skipped без стартане стартовавший прогон (skipped) не имеет started_at — он отклонён до запуска, не оборван в работе
один активный прогонвторой незавершённый прогон агента отвергается partial unique index UNIQUE (agent_id) WHERE state IN ('queued','running') — гонку (ручной запуск совпал с наступившим слотом) гасит сама база, не только проверка в коде
протухший heartbeat → staleворкер умер, не закрыв running → по протуханию heartbeat_at прогон жнётся в failed с reason=stale, замок отпускается, агент стартует снова — зомби его не запирает
Integration · P1 Бюджет, расписание, контроль, изоляция
test_budget.py
Недельный токен-лимит на пользователя
Кейсы
недельная суммарасход = SUM(tokens_used) по всем агентам владельца за текущую неделю (по finished_at), а не на агента
превышение → skippedлимит исчерпан → прогон не стартует, пишется skipped с reason=budget_exceeded, tokens_used=0
сброс неделисмена недельного окна → расход обнуляется, агенты владельца снова стартуют
лимит из настроекзначение лимита берётся из platform_settings, в модуле не хранится — меняется централизованно
test_scheduling.py
Ручной + интервал/календарь, next_run_at, постановка в очередь
IntegrationP1 → Расписание
Кейсы
только ручнойschedule NULL → агент запускается лишь вручную, планировщик его не трогает
интервалинтервальное расписание считает следующий next_run_at от прошлого запуска
календарь (time-machine)календарный график — ежедневно / еженедельно в HH:MM — даёт верный next_run_at
пояс владельцакалендарный HH:MM резолвится против users.timezone владельца (при NULL — org-default platform_settings.timezone), затем хранится UTC
постановка в очередьпланировщик по наступлению next_run_at ставит прогон в фоновую очередь с trigger=scheduled
overlap не плодитслот (или ручной запуск) поверх ещё running/queued прогона нового цикла не создаёт — строка skipped, reason=already_running
ручной без расписания«Запустить сейчас» (trigger=manual) расписания не требует — работает и при schedule=NULL; замки (enabled, admin_paused) при этом всё равно чтятся
test_governance.py
Два замка (enabled · admin_paused), деактивация, каскады учётки
IntegrationP1 → Два замка
Кейсы
admin_paused липкийвладелец снять admin_paused не может — только администратор; попытка владельца не меняет флаг
условия стартаагент стартует — по расписанию или вручную — лишь при enabled, не admin_paused, бюджете в пределах И model_id IS NOT NULL; оси независимы
снятая модель → стопadmin убрал модель из agent_modelsON DELETE SET NULL обнуляет agents.model_id → агента нет в скане (next_run_at обнулён), запуск недоступен, записи skipped нет — как при закрытом замке; на дефолт не откатывается
закрытый замок — тихо, без строкипри enabled=false или admin_paused=true агента нет в скане планировщика (next_run_at обнулён) и кнопка ручного запуска недоступна — прогон не идёт и записи skipped не появляется; durable-стоп журнала не копит
деактивация → enabled=falseдеактивация учётки владельца гасит enabled всем его агентам; реактивация флаг не возвращает — оживляет только сам владелец вручную
удаление → каскадудаление учётки → agents и за ними agent_runs уходят по ON DELETE CASCADE
баннер — на surface«приостановлен администратором» рисуется на стороне surface из флага; домен лишь держит admin_paused
test_isolation.py
Фоновая полоса: свой пул, отдельный потолок LLM, read-only
Кейсы
отдельный пул воркеровпрогоны идут фоновой полосой со своим пулом, не на пути живого чата — нагрузка агентов не влезает в интерактивный путь
отдельный потолок LLMлимит одновременных вызовов модели у фоновой полосы отделён от потолка живого чата (параллелизм)
read-only параллельно записямчтение Knowledge Store идёт параллельно записям графа без блокировок — агент не встаёт в очередь за загрузкой данных
API · P1 Эндпоинты — HTTP-контракт httpx · ASGI · БД · → HTTP API → Conformance
test_api_agents.py
CRUD своих агентов, запуск, чтение журнала
Кейсы
CRUD своихсоздание / правка / удаление своих агентов по HTTP — имя, описание, промт, модель, расписание, флаг enabled
выбор моделиmodel_id из разрешённого списка сохраняется и применяется в прогоне; при создании поле проставляется (преднабор — дефолт списка)
модель не из списка → 422model_id вне agent_models (или embedding-тип) → 422, не 500
запустить сейчас«Запустить сейчас» → 202 + id прогона; создан agent_run (trigger=manual, queued), задача в SAQ
повторный /run под активным прогономвторой POST /agents/{id}/run, пока прогон агента уже running409 CONFLICT (single-flight, не задваивает прогон); согласовано с доменным already_running → skipped
/run по несуществующему агенту → 404неизвестный {id}404, не 500
/run при исчерпанном бюджетебюджет агента выбран → POST /run не стартует прогон: 409 (или доменный skipped с причиной budget_exceeded) — зафиксирован HTTP-исход, не 500
CHECK наружу как 422невалидный trigger / state не из формы, пустое name422, не 500
форма schedule → 422schedule — размеченное объединение по type (interval / calendar), weekday 0–6, time HH:MM; кривой на входе отбит валидацией: (а) неизвестный type; (б) calendar без time; (в) weekday вне 0–6 → 422, не 500 → Форма расписания
чтение журналаGET прогонов агента с output, tokens_used и исходом; skipped с reason отдаётся отлично от failed
test_api_admin.py
Реестр всех агентов, профиль read-only, замок админа
Кейсы
список всехадмин видит ВСЕ агенты платформы — поиск и фильтры по владельцу / состоянию
оба замка в реестререестр отдаёт и выключатель владельца (enabled, read-only для админа), и admin_paused — сводный статус строки виден админу
профиль read-onlyадмин открывает профиль чужого агента только на чтение: владелец, промт, расписание, прогоны
замок / снятиеадмин ставит и снимает admin_paused на любом агенте; выключатель enabled владельца не трогает
без удаления чужогоадминский роут не удаляет чужого агента — удаление остаётся за владельцем (или каскадом учётки)
владелец не снимает чужой admin_pausedвладелец на админском действии над admin_paused403
test_api_access.py
Владелец видит своё, роли и аутентификация на каждом роуте
Кейсы
только своивладелец управляет лишь своими агентами; чужой агент в его роутах не виден и не правится
member заблокированmember на чужого агента или на админский роут → 403 FORBIDDEN
анонимбез токена → 401 UNAUTHORIZED (параметризовано по всем роутам)
истёкший токенпросроченный access JWT → 401
роуты за guardкаждый эндпоинт Agent Engine реально под guard — параметризованный аудит покрытия; сквозную механику RBAC / JWT держит Auth & Security, здесь проверяем лишь, что роуты за ним
Structure Файловая структура тестов

Разделение задаётся назначением, не папками. 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, учёт токенов, timestamps
      • test_budget · test_scheduling · test_governance · test_isolationнедельный бюджет, расписание, замки и каскады, фоновая полоса
    • api/HTTP-контракт эндпоинтов — httpx + ASGI + БД
      • test_api_agents · _admin · _accessCRUD и запуск, админский реестр и замок, роли и аутентификация