← AI Foundation

Тест-кейсы

ai-foundation · workzone
Принятые решения
Реестр в core, читатели только читают Строки заводят Admin (провайдеры / модели) и рантайм (расход); потребители — Harvester, Query Engine, Agent Engine — из реестра лишь читают, строк не создают. Констрейнты бьём через ORM прямо в тест-БД: testcontainers PostgreSQL, реальная схема из миграции, per-test rollback.
RESTRICT — источник истины целостности назначений Занятую модель нельзя удалить или переназначить, пока на неё ссылаются model_assignments / chat_models / agent_models. Замок держит БД (FK RESTRICT), не только проверка в коде — тест обходит сервисный слой и бьёт прямо в базу.
Границы охвата — только БД Здесь контрольный слой: констрейнты, каскады, upsert-агрегат, засев и обратимость. Коннективити-чек провайдера и discovery моделей — логика модуля, её покрывает отдельная итерация; тут не трогаем.
Стек и инфраструктура
Имеется done pytest, pytest-asyncio
Тест-БД testcontainers (PostgreSQL) — реальная схема, per-test rollback; констрейнты проверяются в живой базе, не в моках
Засев проверяется прогоном самой миграции — Platform-провайдер, встроенные модели, tool-пресеты; не хардкод-фикстурой
Маркеры @pytest.mark.integration — весь слой требует БД; приоритет (p0 / p1) ортогонально типу
Integration · P0 Замок занятой модели — RESTRICT
test_model_lifecycle_lock.py
Модель занята — удаление / переназначение отбито БД
IntegrationP0 → Назначения
Кейсы
занята назначениеммодель в model_assignments → DELETE ai_models отбит (FK RESTRICT → IntegrityError)
занята чатоммодель в chat_models → удаление / выключение отбито RESTRICT
занята агентамимодель в agent_models → отбито тем же RESTRICT
свободная удаляетсянигде не ссылаются → DELETE проходит штатно
замок в БДобход сервисного слоя — прямой DELETE в базу тоже отбит; не только проверка в коде
Integration · P1 Дефолт, каскады, upsert-расход, каталог, засев
test_default_model.py
Ровно один default — partial UNIQUE WHERE is_default
Кейсы
два default → отбойвторой is_default=true в chat_models отбит partial unique
смена default атомарнаназначение нового default одной транзакцией снимает старый
нельзя остаться без defaultснять единственный is_default, не назначив новый → запрет (инвариант «ровно один» доводит приложение)
то же для агентовте же три кейса на agent_models — форма списка общая
test_provider_cascade.py
Провайдер / модель: каскад, CHECK, дубль, маска ключа
Кейсы
каскад провайдераDELETE ai_providers → его ai_models уходят по ON DELETE CASCADE
local без base_url → отбойCHECK (kind='cloud' OR base_url IS NOT NULL) — local без endpoint отвергается БД
дубль модели → отбойUNIQUE(provider_id, model_id) — тот же model_id у провайдера дважды отбит
ключ write-onlyнаружу (сериализация / API) уходит маска ••••xxxx, не api_key_enc и не открытый ключ
assignment CHECK — системное подмножествоmodel_assignments.function CHECK-ается словарём {harvester_embedding · query_rag}; INSERT с function='chat' / 'agent_engine' → отбит БД (назначение возможно лишь на системные функции)
usage — полный словарь функцийmodel_usage.function='chat' проходит: расход учитывается по всем четырём функциям, включая chat / agent_engine; два разных словаря на одну колонку → Словарь функций
test_model_usage_upsert.py
Расход: upsert по бакету, SET NULL, вывод cost
IntegrationP1 → model_usage
Кейсы
upsert в тот же бакетповтор по UNIQUE(model_id, function, bucket_date) → одна строка, счётчики (request_count · input_tokens · output_tokens) растут
разный срез → своя строкаиная function / bucket_date → отдельная строка агрегата
удаление модели → SET NULLmodel_id обнуляется по ON DELETE SET NULL, строка расхода живёт — cost переживает
вывод costcost = input·price_input + output·price_output; хоть одна цена NULLcost NULL
test_tools_registry.py
Каталог инструментов: UNIQUE, секрет, CHECK'и
IntegrationP1 → tools
Кейсы
имя уникальноUNIQUE(name) — дубль ключа инструмента отбит
секрет write-onlycredential_enc наружу не уходит — только флаг is_set; config (JSONB) несекретный, отдаётся как есть
CHECK sourcesource ∈ (preset · custom · mcp · openapi) — иное отвергается БД
CHECK accessaccess ∈ (read_only · write) — иное отвергается
test_seed_migration.py
Засев Platform + встроенных, обратимость downgrade
IntegrationP1 → Миграция
Кейсы
Platform-провайдермиграция засевает is_system-провайдера Platform — неудаляем
прямой DELETE отбитудаление is_system-провайдера Platform в обход сервисного слоя — прямой DELETE в базу отбит замком БД, не только проверкой в коде
встроенные + интринсикиembedding-модели засеяны с meta: bge-m3 {embedding_dim:1024, max_input_tokens:8192}, Qwen3-Embedding-0.6B {1024, 32768}
tool-пресеты выключеныдва пресета (web_search · fetch_url), оба chat_enabled=false и agents_allowed=false
downgrade зеркалитDROP в обратном порядке; после отката таблицы пусты
API · P1 Реестр — HTTP-контракт httpx · ASGI-приложение · → HTTP API → Conformance
test_api_registry.py
CRUD провайдеров / моделей / инструментов и назначения
Кейсы
CRUD реестрасоздание / правка / удаление провайдера, модели, инструмента по HTTP → коды; в ответе маска ••••xxxx / флаг is_set, не секрет
назначение функциисмена default чата / агента / эмбеддинга через assignment → 200, старый снят атомарно
занятое под RESTRICT → 409удаление провайдера / модели, назначенной функции → 409, не 500 (замок занятой модели)
CHECK наружу как 422local без base_url, дубль model_id, кривой source/access инструмента → 422, не 500
тип решает назначение → 422model_type гейтит: embedding-модель в chat_models/agent_models или на query_rag422; chat-модель на embedding-функцию → 422 — назначение по чужому типу отбито, не 500 → Тип решает назначение
неизвестный {id} → 404GET/PATCH/DELETE провайдера / модели / инструмента по несуществующему {id}404, не 500
CRUD — только Owner/AdminMember на POST/PATCH/DELETE реестра (провайдеры · модели · инструменты · назначения) → 403; аноним → 401; параметризовано по всем write-роутам, не только discovery
test_api_discovery.py
Опрос каталога моделей у провайдера · доступ
Кейсы
discovery по adapterзапрос каталога → список моделей от облачного (OpenAI-совм. /v1/models) или local (Ollama /api/tags) провайдера; discovery — конфиг, не инференс
провайдер недоступенкривой ключ / недостижимый endpoint → аккуратная ошибка апстрима наружу, не 500
только Owner/AdminCRUD и discovery — Owner/Admin → 200; Member → 403; аноним → 401 (параметризовано по роутам реестра)
Вне охвата Где покрыто рядом
prompt_settings singleton и его CHECK (id = 1), override / reset / кап токенов — покрыты со стороны Admin (test_prompt_settings.py); здесь не дублируется
Structure Файловая структура тестов

Весь слой — integration с БД. Приоритет (P0–P1) ортогонален каталогам и задаётся маркерами (pytest -m p0), а не отдельными папками.

  • tests/ai_foundation/каталог модуля
    • conftest.pytestcontainers PG, фабрики провайдер / модель
    • integration/с БД, per-test rollback
      • model-lifecycle-lockзамок RESTRICT занятой модели
      • default-model · provider-cascadeровно один default, каскад / CHECK / маска
      • model-usage-upsert · tools-registryupsert-расход, каталог инструментов
      • seed-migrationзасев + обратимость downgrade